diff --git a/docs/src/reference/filters.md b/docs/src/reference/filters.md index aaa9614e..93f5154f 100644 --- a/docs/src/reference/filters.md +++ b/docs/src/reference/filters.md @@ -114,6 +114,13 @@ commits that don't match any of the other shas. Produce the history that would be the result of pushing the passed branches with the passed filters into the upstream. +### Prune trivial merge commits **:prune=trivial-merge** + +Produce a history that skips all merge commits whose tree is identical to the first parents +tree. +Normally Josh will keep all commits in the filtered history whose tree differs from any of it's +parents. + Filter order matters -------------------- diff --git a/josh-core/src/filter/mod.rs b/josh-core/src/filter/mod.rs index d6b035ed..fc16344a 100644 --- a/josh-core/src/filter/mod.rs +++ b/josh-core/src/filter/mod.rs @@ -144,6 +144,7 @@ enum Op { Rev(std::collections::BTreeMap), Join(std::collections::BTreeMap), Linear, + Prune, Unsign, RegexReplace(Vec<(regex::Regex, String)>), @@ -485,6 +486,7 @@ fn spec2(op: &Op) -> String { Op::Unsign => ":unsign".to_string(), Op::Subdir(path) => format!(":/{}", parse::quote_if(&path.to_string_lossy())), Op::File(path) => format!("::{}", parse::quote_if(&path.to_string_lossy())), + Op::Prune => ":prune=trivial-merge".to_string(), Op::Prefix(path) => format!(":prefix={}", parse::quote_if(&path.to_string_lossy())), Op::Pattern(pattern) => format!("::{}", parse::quote_if(pattern)), Op::Author(author, email) => { @@ -811,6 +813,33 @@ fn apply_to_commit2( )) .transpose(); } + Op::Prune => { + let p: Vec<_> = commit.parent_ids().collect(); + + if p.len() > 0 { + let parent = some_or!(transaction.get(filter, p[0]), { + return Ok(None); + }); + + let parent_tree = transaction.repo().find_commit(parent)?.tree_id(); + + if parent_tree == commit.tree_id() { + return Ok(Some(history::drop_commit( + commit, + vec![parent], + transaction, + filter, + )?)); + } + } + + RewriteData { + tree: commit.tree()?, + message: None, + author: None, + committer: None, + } + } Op::Unsign => { let parents: Vec<_> = commit.parent_ids().collect(); @@ -998,6 +1027,7 @@ fn apply2<'a>( Op::Committer(_, _) => Ok(tree), Op::Squash(Some(_)) => Err(josh_error("not applicable to tree")), Op::Linear => Ok(tree), + Op::Prune => Ok(tree), Op::Unsign => Ok(tree), Op::Rev(_) => Err(josh_error("not applicable to tree")), Op::Join(_) => Err(josh_error("not applicable to tree")), diff --git a/josh-core/src/filter/opt.rs b/josh-core/src/filter/opt.rs index 96ca4c33..457542e6 100644 --- a/josh-core/src/filter/opt.rs +++ b/josh-core/src/filter/opt.rs @@ -489,6 +489,7 @@ pub fn invert(filter: Filter) -> JoshResult { let result = match to_op(filter) { Op::Nop => Some(Op::Nop), Op::Linear => Some(Op::Nop), + Op::Prune => Some(Op::Prune), Op::Unsign => Some(Op::Unsign), Op::Empty => Some(Op::Empty), Op::Subdir(path) => Some(Op::Prefix(path)), diff --git a/josh-core/src/filter/parse.rs b/josh-core/src/filter/parse.rs index 505661f4..c6588954 100644 --- a/josh-core/src/filter/parse.rs +++ b/josh-core/src/filter/parse.rs @@ -35,6 +35,22 @@ fn make_op(args: &[&str]) -> JoshResult { ["SQUASH"] => Ok(Op::Squash(None)), ["SQUASH", _ids @ ..] => Err(josh_error("SQUASH with ids can't be parsed")), ["linear"] => Ok(Op::Linear), + ["prune", "trivial-merge"] => Ok(Op::Prune), + ["prune"] => Err(josh_error(indoc!( + r#" + Filter ":prune" requires an argument. + + Note: use "=" to provide the argument value: + + :prune=trivial-merge + "# + ))), + ["prune", _] => Err(josh_error(indoc!( + r#" + Filter ":prune" only supports "trivial-merge" + as argument value. + "# + ))), ["unsign"] => Ok(Op::Unsign), ["PATHS"] => Ok(Op::Paths), ["INDEX"] => Ok(Op::Index), diff --git a/tests/filter/prune_trivial_merge.t b/tests/filter/prune_trivial_merge.t new file mode 100644 index 00000000..081b8085 --- /dev/null +++ b/tests/filter/prune_trivial_merge.t @@ -0,0 +1,47 @@ + $ export RUST_BACKTRACE=1 + $ git init -q 1> /dev/null + + $ echo contents1 > file1 + $ git add . + $ git commit -m "add file1" 1> /dev/null + + $ git log --graph --pretty=%s + * add file1 + + $ git checkout -b branch1 + Switched to a new branch 'branch1' + $ echo contents2 > file2 + $ git add . + $ git commit -m "add file2" 1> /dev/null + + $ git checkout master + Switched to branch 'master' + + $ echo contents3 > file1 + $ git add . + $ git commit -m "mod file1" 1> /dev/null + + $ git merge -q branch1 --no-ff + $ git log --graph --pretty=%s + * Merge branch 'branch1' + |\ + | * add file2 + * | mod file1 + |/ + * add file1 + + $ josh-filter -s ::file1 + [3] ::file1 + $ git log --graph --pretty=%s FILTERED_HEAD + * Merge branch 'branch1' + |\ + * | mod file1 + |/ + * add file1 + $ josh-filter -s ::file1:prune=trivial-merge + [2] :prune=trivial-merge + [3] ::file1 + + $ git log --graph --pretty=%s FILTERED_HEAD + * mod file1 + * add file1