diff --git a/docs/src/reference/filters.md b/docs/src/reference/filters.md index 398e43439..0d07c53cf 100644 --- a/docs/src/reference/filters.md +++ b/docs/src/reference/filters.md @@ -65,6 +65,13 @@ workspace root as well as additional files specified in the ``workspace.josh`` f ### Text replacement **`:replace="regex","replacement"`** Applies the supplied regular expression to every file in the input tree. +### Signature removal **`:unsign` +The default behaviour of Josh is to copy, if it exsists, the signature of the original commit in +the filtered commit. This makes the signature invalid, but allows a perfect round-trip: josh will be +able to recreate the original commit from the filtered one. + +This behaviour might not be desirable, and this filter drops the signatures from the history. + ## Pattern filters The following filters accept a glob like pattern ``X`` that can contain ``*`` to diff --git a/josh-proxy/src/lib.rs b/josh-proxy/src/lib.rs index 78ebf498a..89b84b67c 100644 --- a/josh-proxy/src/lib.rs +++ b/josh-proxy/src/lib.rs @@ -403,6 +403,7 @@ fn split_changes( &vec![&parent], &new_tree, None, + false, )?; changes[i].1 = new_commit; new_bases.push(new_commit); diff --git a/src/filter/mod.rs b/src/filter/mod.rs index ed854ef8f..da618f9a1 100644 --- a/src/filter/mod.rs +++ b/src/filter/mod.rs @@ -100,6 +100,7 @@ enum Op { Squash(Option>), Rev(std::collections::HashMap), Linear, + Unsign, RegexReplace(regex::Regex, String), @@ -277,6 +278,7 @@ fn spec2(op: &Op) -> String { format!(":SQUASH={}", s) } Op::Linear => ":linear".to_string(), + Op::Unsign => ":unsign".to_string(), Op::Subdir(path) => format!(":/{}", parse::quote(&path.to_string_lossy())), Op::File(path) => format!("::{}", parse::quote(&path.to_string_lossy())), Op::Prefix(path) => format!(":prefix={}", parse::quote(&path.to_string_lossy())), @@ -387,6 +389,7 @@ fn apply_to_commit2( &[], &commit.tree()?, None, + true, )) .transpose() } @@ -467,6 +470,28 @@ fn apply_to_commit2( )) .transpose(); } + Op::Unsign => { + let parents: Vec<_> = commit.parent_ids().collect(); + + let filtered_parents: Vec<_> = parents + .iter() + .map(|p| transaction.get(filter, *p)) + .collect(); + if filtered_parents.iter().any(|p| p.is_none()) { + return Ok(None); + } + let filtered_parents = filtered_parents.iter().map(|p| p.unwrap()).collect(); + + return Some(history::remove_commit_signature( + commit, + filtered_parents, + commit.tree()?, + transaction, + filter, + None, + )) + .transpose(); + } Op::Compose(filters) => { let filtered = filters .iter() @@ -661,6 +686,7 @@ fn apply2<'a>( Op::Squash(None) => Ok(tree), Op::Squash(Some(_)) => Err(josh_error("not applicable to tree")), Op::Linear => Ok(tree), + Op::Unsign => Ok(tree), Op::Rev(_) => Err(josh_error("not applicable to tree")), Op::RegexReplace(regex, replacement) => { tree::regex_replace(tree.id(), ®ex, &replacement, transaction) diff --git a/src/filter/opt.rs b/src/filter/opt.rs index 868ae0808..6beb8df65 100644 --- a/src/filter/opt.rs +++ b/src/filter/opt.rs @@ -422,6 +422,7 @@ pub fn invert(filter: Filter) -> JoshResult { let result = match to_op(filter) { Op::Nop => Some(Op::Nop), Op::Linear => Some(Op::Nop), + Op::Unsign => Some(Op::Unsign), Op::Empty => Some(Op::Empty), Op::Subdir(path) => Some(Op::Prefix(path)), Op::File(path) => Some(Op::File(path)), diff --git a/src/filter/parse.rs b/src/filter/parse.rs index dbf084568..4127cfcbb 100644 --- a/src/filter/parse.rs +++ b/src/filter/parse.rs @@ -37,6 +37,7 @@ 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), + ["unsign"] => Ok(Op::Unsign), ["PATHS"] => Ok(Op::Paths), #[cfg(feature = "search")] ["INDEX"] => Ok(Op::Index), diff --git a/src/history.rs b/src/history.rs index 21a70c6c7..25eea1953 100644 --- a/src/history.rs +++ b/src/history.rs @@ -182,13 +182,8 @@ pub fn rewrite_commit( parents: &[&git2::Commit], tree: &git2::Tree, message: Option<(String, String, String)>, + unsign: bool, ) -> JoshResult { - if message == None && base.tree()?.id() == tree.id() && all_equal(base.parents(), parents) { - // Looks like an optimization, but in fact serves to not change the commit in case - // it was signed. - return Ok(base.id()); - } - let b = if let Some((message, author, email)) = message { let a = base.author(); let new_a = git2::Signature::new(&author, &email, &a.when())?; @@ -205,7 +200,7 @@ pub fn rewrite_commit( )? }; - if let Ok((sig, _)) = repo.extract_signature(&base.id(), None) { + if let (false, Ok((sig, _))) = (unsign, repo.extract_signature(&base.id(), None)) { // Re-create the object with the original signature (which of course does not match any // more, but this is needed to guarantee perfect round-trips). let b = b @@ -220,20 +215,6 @@ pub fn rewrite_commit( return Ok(repo.odb()?.write(git2::ObjectType::Commit, &b)?); } -fn all_equal(a: git2::Parents, b: &[&git2::Commit]) -> bool { - let a: Vec<_> = a.collect(); - if a.len() != b.len() { - return false; - } - - for (x, y) in b.iter().zip(a.iter()) { - if x.id() != y.id() { - return false; - } - } - true -} - fn find_oldest_similar_commit( transaction: &cache::Transaction, filter: filter::Filter, @@ -527,6 +508,7 @@ pub fn unapply_filter( &original_parents_refs, &new_tree, None, + false, )?; if let Some(ref mut change_ids) = change_ids { @@ -560,6 +542,30 @@ fn select_parent_commits<'a>( } } +pub fn remove_commit_signature<'a>( + original_commit: &'a git2::Commit, + filtered_parent_ids: Vec, + filtered_tree: git2::Tree<'a>, + transaction: &cache::Transaction, + filter: filter::Filter, + message: Option<(String, String, String)>, +) -> JoshResult { + let (r, is_new) = create_filtered_commit2( + transaction.repo(), + original_commit, + filtered_parent_ids, + filtered_tree, + message, + true, + )?; + + let store = is_new || original_commit.parent_ids().len() != 1; + + transaction.insert(filter, original_commit.id(), r, store); + + Ok(r) +} + pub fn drop_commit<'a>( original_commit: &'a git2::Commit, filtered_parent_ids: Vec, @@ -591,6 +597,7 @@ pub fn create_filtered_commit<'a>( filtered_parent_ids, filtered_tree, message, + false, )?; let store = is_new || original_commit.parent_ids().len() != 1; @@ -606,6 +613,7 @@ fn create_filtered_commit2<'a>( filtered_parent_ids: Vec, filtered_tree: git2::Tree<'a>, message: Option<(String, String, String)>, + unsign: bool, ) -> JoshResult<(git2::Oid, bool)> { let filtered_parent_commits: Result, _> = filtered_parent_ids .iter() @@ -651,6 +659,7 @@ fn create_filtered_commit2<'a>( &selected_filtered_parent_commits, &filtered_tree, message, + unsign, )?, true, )) diff --git a/tests/filter/gpgsig.t b/tests/filter/gpgsig.t index c29e3caba..cfdd817e8 100644 --- a/tests/filter/gpgsig.t +++ b/tests/filter/gpgsig.t @@ -35,3 +35,25 @@ If 0b4cf6c9efbbda1eada39fa9c1d21d2525b027bb shows up then the signature was lost $ git rev-parse master double-filtered cb22ebb8e47b109f7add68b1043e561e0db09802 cb22ebb8e47b109f7add68b1043e561e0db09802 + +Remove the signature, the shas are different. + $ josh-filter :unsign refs/heads/master --update refs/heads/filtered -s + [1] :unsign + $ git rev-parse master filtered + cb22ebb8e47b109f7add68b1043e561e0db09802 + 0b4cf6c9efbbda1eada39fa9c1d21d2525b027bb + $ josh-filter --reverse :unsign refs/heads/double-filtered --update refs/heads/filtered -s + [1] :unsign + $ git rev-parse master double-filtered + cb22ebb8e47b109f7add68b1043e561e0db09802 + cb22ebb8e47b109f7add68b1043e561e0db09802 + +Round trip does not work but reversed works since the commit exists + $ josh-filter :prefix=extra:unsign refs/heads/master --update refs/heads/filtered + $ josh-filter :/extra refs/heads/filtered --update refs/heads/double-filtered + $ git branch reversed + $ josh-filter --reverse :prefix=extra:unsign refs/heads/reversed --update refs/heads/filtered + $ git rev-parse master double-filtered reversed + cb22ebb8e47b109f7add68b1043e561e0db09802 + 0b4cf6c9efbbda1eada39fa9c1d21d2525b027bb + cb22ebb8e47b109f7add68b1043e561e0db09802