From 5d320f5eb226a8b4f37d90b4a6d859b284626013 Mon Sep 17 00:00:00 2001 From: Christian Schilling Date: Fri, 19 Sep 2025 14:50:44 +0200 Subject: [PATCH] Add :prune=trivial-merge filter The default history simplification will not drop merge commits if their tree differs from any parent. In most cases however merges that don't differ from the first parent are not wanted in the output history. This new filter allows to prune them. Change: prune-trivial-merges --- docs/src/reference/filters.md | 7 +++++ josh-core/src/filter/mod.rs | 30 +++++++++++++++++++ josh-core/src/filter/opt.rs | 1 + josh-core/src/filter/parse.rs | 16 ++++++++++ tests/filter/prune_trivial_merge.t | 47 ++++++++++++++++++++++++++++++ 5 files changed, 101 insertions(+) create mode 100644 tests/filter/prune_trivial_merge.t 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..e901d159 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 arguement 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