Skip to content

Commit

Permalink
Implement squash filter for list of commits (#948)
Browse files Browse the repository at this point in the history
  • Loading branch information
christian-schilling authored Oct 31, 2022
1 parent 11227ec commit 894bedb
Show file tree
Hide file tree
Showing 8 changed files with 290 additions and 59 deletions.
1 change: 1 addition & 0 deletions josh-proxy/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,7 @@ fn split_changes(
&repo.find_commit(changes[i].1)?,
&vec![&parent],
&new_tree,
None,
)?;
changes[i].1 = new_commit;
new_bases.push(new_commit);
Expand Down
124 changes: 83 additions & 41 deletions src/bin/josh-filter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,24 @@ fn make_app() -> clap::Command {
)
.arg(
clap::Arg::new("squash")
.help("Produce a history that contains only commits pointed to by references matching the given pattern")
.long("squash")
)
.arg(
clap::Arg::new("author")
.help("Author to use for commits with rewritten message")
.long("author")
)
.arg(
clap::Arg::new("email")
.help("Author email to use for commits with rewritten message")
.long("email")
)
.arg(
clap::Arg::new("single")
.action(clap::ArgAction::SetTrue)
.help("Only output one commit, without history")
.long("squash"),
.help("Produce a history that contains only one single commit")
.long("single"),
)
.arg(
clap::Arg::new("discover")
Expand Down Expand Up @@ -138,10 +153,6 @@ fn run_filter(args: Vec<String>) -> josh::JoshResult<i32> {

let mut filterobj = josh::filter::parse(&specstr)?;

if args.get_flag("squash") {
filterobj = josh::filter::chain(josh::filter::parse(":SQUASH")?, filterobj);
}

if args.get_flag("print-filter") {
let filterobj = if args.get_flag("reverse") {
josh::filter::invert(filterobj)?
Expand All @@ -162,6 +173,38 @@ fn run_filter(args: Vec<String>) -> josh::JoshResult<i32> {
let transaction = josh::cache::Transaction::new(repo, None);
let repo = transaction.repo();

let input_ref = args.get_one::<String>("input").unwrap();

let mut refs = vec![];
let mut ids = vec![];

let reference = repo.resolve_reference_from_short_name(input_ref).unwrap();
let input_ref = reference.name().unwrap().to_string();
refs.push((input_ref.clone(), reference.target().unwrap()));

if args.get_flag("single") {
filterobj = josh::filter::chain(josh::filter::squash(None), filterobj);
}

if let Some(pattern) = args.get_one::<String>("squash") {
let pattern = pattern.to_string();
for reference in repo.references_glob(&pattern).unwrap() {
let reference = reference?;
if let Some(target) = reference.target() {
ids.push((target, reference.name().unwrap().to_string()));
refs.push((reference.name().unwrap().to_string(), target));
}
}
filterobj = josh::filter::chain(
josh::filter::squash(Some((
args.get_one::<String>("author").unwrap(),
args.get_one::<String>("email").unwrap(),
&ids,
))),
filterobj,
);
};

let odb = repo.odb()?;
let mp = if args.get_flag("pack") {
let mempack = odb.add_new_mempack_backend(1000)?;
Expand All @@ -188,10 +231,8 @@ fn run_filter(args: Vec<String>) -> josh::JoshResult<i32> {
}
});

let input_ref = args.get_one::<String>("input").unwrap();

if args.get_flag("discover") {
let r = repo.revparse_single(input_ref)?;
let r = repo.revparse_single(&input_ref)?;
let hs = josh::housekeeping::find_all_workspaces_and_subdirectories(&r.peel_to_tree()?)?;
for i in hs {
if i.contains(":workspace=") {
Expand All @@ -210,23 +251,10 @@ fn run_filter(args: Vec<String>) -> josh::JoshResult<i32> {

let update_target = args.get_one::<String>("update").unwrap();

let src = input_ref;
let target = update_target;

let reverse = args.get_flag("reverse");

let t = if reverse {
"refs/JOSH_TMP".to_owned()
} else {
target.to_string()
};
let src_r = repo
.revparse_ext(src)?
.1
.ok_or(josh::josh_error("reference not found"))?;

let src = src_r.name().unwrap().to_string();

let check_permissions = args.get_flag("check-permission");
let mut permissions_filter = josh::filter::empty();
if check_permissions {
Expand Down Expand Up @@ -264,28 +292,31 @@ fn run_filter(args: Vec<String>) -> josh::JoshResult<i32> {
permissions_filter = josh::filter::empty();
}

let old_oid = if let Ok(id) = transaction.repo().refname_to_id(&t) {
let old_oid = if let Ok(id) = transaction.repo().refname_to_id(&target) {
id
} else {
git2::Oid::zero()
};
let mut updated_refs = josh::filter_refs(
&transaction,
filterobj,
&[(src.clone(), src_r.target().unwrap())],
permissions_filter,
)?;
updated_refs[0].0 = t;
josh::update_refs(&transaction, &mut updated_refs, "");
if args.get_one::<String>("update").map(|v| v.as_str()) != Some("FILTERED_HEAD")
&& updated_refs.len() == 1
&& updated_refs[0].1 == old_oid
{
println!(
"Warning: reference {} wasn't updated",
args.get_one::<String>("update").unwrap()
);

let mut updated_refs = josh::filter_refs(&transaction, filterobj, &refs, permissions_filter)?;
for i in 0..updated_refs.len() {
if updated_refs[i].0 == input_ref {
if reverse {
updated_refs[i].0 = "refs/JOSH_TMP".to_string();
} else {
updated_refs[i].0 = target.to_string();
}
} else {
updated_refs[i].0 =
updated_refs[i]
.0
.replacen("refs/heads/", "refs/heads/filtered/", 1);
updated_refs[i].0 = updated_refs[i]
.0
.replacen("refs/tags/", "refs/tags/filtered/", 1);
}
}
josh::update_refs(&transaction, &mut updated_refs, "");

#[cfg(feature = "search")]
if let Some(searchstring) = args.get_one::<String>("search") {
Expand Down Expand Up @@ -324,7 +355,7 @@ fn run_filter(args: Vec<String>) -> josh::JoshResult<i32> {
if reverse {
let new = repo.revparse_single(target).unwrap().id();
let old = repo.revparse_single("JOSH_TMP").unwrap().id();
let unfiltered_old = repo.revparse_single(input_ref).unwrap().id();
let unfiltered_old = repo.revparse_single(&input_ref).unwrap().id();

match josh::history::unapply_filter(
&transaction,
Expand All @@ -337,7 +368,7 @@ fn run_filter(args: Vec<String>) -> josh::JoshResult<i32> {
&mut None,
) {
Ok(rewritten) => {
repo.reference(&src, rewritten, true, "unapply_filter")?;
repo.reference(&input_ref, rewritten, true, "unapply_filter")?;
}
Err(JoshError(msg)) => {
println!("{}", msg);
Expand All @@ -346,6 +377,17 @@ fn run_filter(args: Vec<String>) -> josh::JoshResult<i32> {
}
}

if !reverse
&& args.get_one::<String>("update") != Some(&"FILTERED_HEAD".to_string())
&& updated_refs.len() == 1
&& updated_refs[0].1 == old_oid
{
println!(
"Warning: reference {} wasn't updated",
args.get_one::<String>("update").unwrap()
);
}

if let Some(gql_query) = args.get_one::<String>("graphql") {
let context = josh::graphql::context(transaction.try_clone()?, transaction.try_clone()?);
*context.allow_refs.lock()? = true;
Expand Down
6 changes: 3 additions & 3 deletions src/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,10 @@ pub fn print_stats() {
let name = String::from_utf8(name.to_vec()).unwrap();
let t = db.open_tree(&name).unwrap();
if !t.is_empty() {
let name = if name.contains("SUBTRACT") || name.starts_with('_') {
name.clone()
let name = if let Ok(filter) = filter::parse(&name) {
filter::pretty(filter, 4)
} else {
filter::pretty(filter::parse(&name).unwrap(), 4)
name.clone()
};
v.push((t.len(), name));
}
Expand Down
70 changes: 65 additions & 5 deletions src/filter/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,18 @@ pub fn empty() -> Filter {
to_filter(Op::Empty)
}

pub fn squash(ids: Option<(&str, &str, &[(git2::Oid, String)])>) -> Filter {
if let Some((author, email, ids)) = ids {
to_filter(Op::Squash(Some(
ids.iter()
.map(|(x, y)| (*x, (y.clone(), author.to_string(), email.to_string())))
.collect(),
)))
} else {
to_filter(Op::Squash(None))
}
}

fn to_filter(op: Op) -> Filter {
let s = format!("{:?}", op);
let f = Filter(
Expand All @@ -85,7 +97,7 @@ enum Op {
Empty,
Fold,
Paths,
Squash,
Squash(Option<std::collections::HashMap<git2::Oid, (String, String, String)>>),
Linear,

RegexReplace(regex::Regex, String),
Expand Down Expand Up @@ -236,7 +248,18 @@ fn spec2(op: &Op) -> String {
#[cfg(feature = "search")]
Op::Index => ":INDEX".to_string(),
Op::Fold => ":FOLD".to_string(),
Op::Squash => ":SQUASH".to_string(),
Op::Squash(None) => ":SQUASH".to_string(),
Op::Squash(Some(hs)) => {
let mut v = hs
.iter()
.map(|(x, y)| format!("{}:{}:{}:{}", x, y.0, y.1, y.2))
.collect::<Vec<String>>();
v.sort();
let s = v.join(",");
let s = git2::Oid::hash_object(git2::ObjectType::Blob, s.as_bytes())
.expect("hash_object filter");
format!(":SQUASH={}", s)
}
Op::Linear => ":linear".to_string(),
Op::Subdir(path) => format!(":/{}", parse::quote(&path.to_string_lossy())),
Op::File(path) => format!("::{}", parse::quote(&path.to_string_lossy())),
Expand Down Expand Up @@ -341,8 +364,15 @@ fn apply_to_commit2(
Ok(Some(git2::Oid::zero()))
};
}
Op::Squash => {
return Some(history::rewrite_commit(repo, commit, &[], &commit.tree()?)).transpose()
Op::Squash(None) => {
return Some(history::rewrite_commit(
repo,
commit,
&[],
&commit.tree()?,
None,
))
.transpose()
}
_ => {
if let Some(oid) = transaction.get(filter, commit.id()) {
Expand All @@ -354,6 +384,27 @@ fn apply_to_commit2(
rs_tracing::trace_scoped!("apply_to_commit", "spec": spec(filter), "commit": commit.id().to_string());

let filtered_tree = match &to_op(filter) {
Op::Squash(Some(ids)) => {
if let Some(_) = ids.get(&commit.id()) {
commit.tree()?
} else {
for parent in commit.parents() {
return Ok(
if let Some(fparent) = transaction.get(filter, parent.id()) {
Some(history::drop_commit(
commit,
vec![fparent],
transaction,
filter,
)?)
} else {
None
},
);
}
tree::empty(repo)
}
}
Op::Linear => {
let p: Vec<_> = commit.parent_ids().collect();
if p.is_empty() {
Expand All @@ -370,6 +421,7 @@ fn apply_to_commit2(
commit.tree()?,
transaction,
filter,
None,
))
.transpose();
}
Expand Down Expand Up @@ -452,6 +504,7 @@ fn apply_to_commit2(
filtered_tree,
transaction,
filter,
None,
))
.transpose();
}
Expand Down Expand Up @@ -528,12 +581,18 @@ fn apply_to_commit2(

let filtered_parent_ids = some_or!(filtered_parent_ids, { return Ok(None) });

let message = match to_op(filter) {
Op::Squash(Some(ids)) => ids.get(&commit.id()).map(|x| x.clone()),
_ => None,
};

Some(history::create_filtered_commit(
commit,
filtered_parent_ids,
filtered_tree,
transaction,
filter,
message,
))
.transpose()
}
Expand All @@ -557,7 +616,8 @@ fn apply2<'a>(
Op::Nop => Ok(tree),
Op::Empty => return Ok(tree::empty(repo)),
Op::Fold => Ok(tree),
Op::Squash => Ok(tree),
Op::Squash(None) => Ok(tree),
Op::Squash(Some(_)) => Err(josh_error("not applicable to tree")),
Op::Linear => Ok(tree),

Op::RegexReplace(regex, replacement) => {
Expand Down
3 changes: 2 additions & 1 deletion src/filter/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ fn make_op(args: &[&str]) -> JoshResult<Op> {
Where `path` is path to the directory where workspace.josh file is located
"#
))),
["SQUASH"] => Ok(Op::Squash),
["SQUASH"] => Ok(Op::Squash(None)),
["SQUASH", _ids @ ..] => Err(josh_error("SQUASH with ids can't be parsed")),
["linear"] => Ok(Op::Linear),
["PATHS"] => Ok(Op::Paths),
#[cfg(feature = "search")]
Expand Down
Loading

0 comments on commit 894bedb

Please sign in to comment.