diff --git a/Cargo.lock b/Cargo.lock index fac24821..6676506f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1581,6 +1581,7 @@ dependencies = [ "serde_json", "serde_yaml", "sled", + "strfmt", "tracing", ] diff --git a/josh-core/Cargo.toml b/josh-core/Cargo.toml index 805e6b95..9bf4a839 100644 --- a/josh-core/Cargo.toml +++ b/josh-core/Cargo.toml @@ -26,6 +26,7 @@ pest_derive = "2.7.10" rayon = "1.10.0" regex = { workspace = true } rs_tracing = { workspace = true } +strfmt = "0.2.4" serde = { workspace = true } serde_json = { workspace = true } serde_yaml = { workspace = true } diff --git a/josh-core/src/filter/grammar.pest b/josh-core/src/filter/grammar.pest index 385bfd72..0a6a28be 100644 --- a/josh-core/src/filter/grammar.pest +++ b/josh-core/src/filter/grammar.pest @@ -22,6 +22,7 @@ char = { filter_spec = { ( filter_group + | filter_message | filter_rev | filter_join | filter_replace @@ -39,6 +40,7 @@ filter_nop = { CMD_START ~ "/" } filter_presub = { CMD_START ~ ":" ~ argument } filter = { CMD_START ~ cmd ~ "=" ~ (argument ~ (";" ~ argument)*)? } filter_noarg = { CMD_START ~ cmd } +filter_message = { CMD_START ~ string } filter_rev = { CMD_START ~ "rev" ~ "(" @@ -71,8 +73,8 @@ filter_replace = { filter_squash = { CMD_START ~ "squash" ~ "(" ~ NEWLINE* - ~ (rev ~ ":" ~ string)? - ~ (CMD_SEP+ ~ (rev ~ ":" ~ string))* + ~ (rev ~ filter_spec)? + ~ (CMD_SEP+ ~ (rev ~ filter_spec))* ~ NEWLINE* ~ ")" } diff --git a/josh-core/src/filter/mod.rs b/josh-core/src/filter/mod.rs index 0ccf177e..9c818bd8 100644 --- a/josh-core/src/filter/mod.rs +++ b/josh-core/src/filter/mod.rs @@ -1,4 +1,5 @@ use super::*; +use history::RewriteData; use pest::Parser; use std::path::Path; mod opt; @@ -65,7 +66,11 @@ pub fn empty() -> Filter { to_filter(Op::Empty) } -pub fn squash(ids: Option<&[(git2::Oid, String)]>) -> Filter { +pub fn message(m: &str) -> Filter { + to_filter(Op::Message(m.to_string())) +} + +pub fn squash(ids: Option<&[(git2::Oid, Filter)]>) -> Filter { if let Some(ids) = ids { to_filter(Op::Squash(Some( ids.iter() @@ -130,7 +135,7 @@ enum Op { // We use BTreeMap rather than HashMap to guarantee deterministic results when // converting to Filter - Squash(Option>), + Squash(Option>), Author(String, String), // We use BTreeMap rather than HashMap to guarantee deterministic results when @@ -151,6 +156,7 @@ enum Op { Workspace(std::path::PathBuf), Glob(String), + Message(String), Compose(Vec), Chain(Filter, Filter), @@ -235,14 +241,7 @@ fn pretty2(op: &Op, indent: usize, compose: bool) -> String { Op::Squash(Some(ids)) => { let mut v = ids .iter() - .map(|(oid, msg)| { - format!( - "{}{}:{}", - " ".repeat(indent), - &oid.to_string(), - parse::quote(msg) - ) - }) + .map(|(oid, f)| format!("{}{}{}", " ".repeat(indent), &oid.to_string(), spec(*f))) .collect::>(); v.sort(); format!(":squash(\n{}\n)", v.join("\n")) @@ -479,7 +478,7 @@ fn spec2(op: &Op) -> String { Op::Squash(Some(ids)) => { let mut v = ids .iter() - .map(|(oid, msg)| format!("{}:{}", oid.to_string(), parse::quote(msg))) + .map(|(oid, f)| format!("{}{}", oid.to_string(), spec(*f))) .collect::>(); v.sort(); format!(":squash({})", v.join(",")) @@ -493,6 +492,9 @@ fn spec2(op: &Op) -> String { Op::Author(author, email) => { format!(":author={};{}", parse::quote(author), parse::quote(email)) } + Op::Message(m) => { + format!(":{}", parse::quote(m)) + } } } @@ -635,9 +637,12 @@ fn apply_to_commit2( repo, commit, &[], - &commit.tree()?, - None, - None, + RewriteData { + tree: commit.tree()?, + author: None, + committer: None, + message: None, + }, true, )) .transpose() @@ -679,7 +684,7 @@ 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) { + let rewrite_data = match &to_op(filter) { Op::Rev(filters) => { let nf = *filters .get(&LazyRef::Resolved(git2::Oid::zero())) @@ -721,11 +726,41 @@ fn apply_to_commit2( } } - apply(transaction, nf, commit.tree()?)? + RewriteData { + tree: apply(transaction, nf, commit.tree()?)?, + message: None, + author: None, + committer: None, + } } Op::Squash(Some(ids)) => { - if ids.get(&LazyRef::Resolved(commit.id())).is_some() { - commit.tree()? + if let Some(sq) = ids.get(&LazyRef::Resolved(commit.id())) { + let oid = if let Some(oid) = + apply_to_commit2(&Op::Chain(filter::squash(None), *sq), commit, transaction)? + { + oid + } else { + return Ok(None); + }; + + let rc = transaction.repo().find_commit(oid)?; + let author = rc + .author() + .name() + .map(|x| x.to_owned()) + .zip(rc.author().email().map(|x| x.to_owned())); + let committer = rc + .committer() + .name() + .map(|x| x.to_owned()) + .zip(rc.committer().email().map(|x| x.to_owned())); + RewriteData { + tree: rc.tree()?, + message: rc.message_raw().map(|x| x.to_owned()), + author: author, + committer: committer, + } + //commit.tree()? } else { if let Some(parent) = commit.parents().next() { return Ok( @@ -762,11 +797,14 @@ fn apply_to_commit2( return Some(history::create_filtered_commit( commit, vec![parent], - commit.tree()?, + RewriteData { + tree: commit.tree()?, + author: None, + committer: None, + message: None, + }, transaction, filter, - None, - None, )) .transpose(); } @@ -847,11 +885,14 @@ fn apply_to_commit2( return Some(history::create_filtered_commit( commit, filtered_parent_ids, - filtered_tree, + RewriteData { + tree: filtered_tree, + author: None, + committer: None, + message: None, + }, transaction, filter, - None, - None, )) .transpose(); } @@ -874,9 +915,36 @@ fn apply_to_commit2( filtered_tree = tree::overlay(transaction, filtered_tree, t)?; } - repo.find_tree(filtered_tree)? + let filtered_tree = repo.find_tree(filtered_tree)?; + RewriteData { + tree: filtered_tree, + author: None, + committer: None, + message: None, + } } - _ => apply(transaction, filter, commit.tree()?)?, + Op::Author(author, email) => RewriteData { + tree: commit.tree()?, + author: Some((author.clone(), email.clone())), + committer: Some((author.clone(), email.clone())), + message: None, + }, + Op::Message(m) => RewriteData { + tree: commit.tree()?, + author: None, + committer: None, + // Pass the message through `strfmt` to enable future extensions + message: Some(strfmt::strfmt( + m, + &std::collections::HashMap::::new(), + )?), + }, + _ => RewriteData { + tree: apply(transaction, filter, commit.tree()?)?, + message: None, + author: None, + committer: None, + }, }; let filtered_parent_ids = { @@ -889,24 +957,12 @@ fn apply_to_commit2( let filtered_parent_ids = some_or!(filtered_parent_ids, { return Ok(None) }); - let author = match to_op(filter) { - Op::Author(author, email) => Some((author, email)), - _ => None, - }; - - let message = match to_op(filter) { - Op::Squash(Some(ids)) => ids.get(&LazyRef::Resolved(commit.id())).cloned(), - _ => None, - }; - Some(history::create_filtered_commit( commit, filtered_parent_ids, - filtered_tree, + rewrite_data, transaction, filter, - author, - message, )) .transpose() } @@ -931,6 +987,7 @@ fn apply2<'a>( Op::Empty => return Ok(tree::empty(repo)), Op::Fold => Ok(tree), Op::Squash(None) => Ok(tree), + Op::Message(_) => Ok(tree), Op::Author(_, _) => Ok(tree), Op::Squash(Some(_)) => Err(josh_error("not applicable to tree")), Op::Linear => Ok(tree), diff --git a/josh-core/src/filter/parse.rs b/josh-core/src/filter/parse.rs index ddd75de8..ac38b3bc 100644 --- a/josh-core/src/filter/parse.rs +++ b/josh-core/src/filter/parse.rs @@ -85,6 +85,10 @@ fn parse_item(pair: pest::iterators::Pair) -> JoshResult { let mut inner = pair.into_inner(); make_op(&[inner.next().unwrap().as_str()]) } + Rule::filter_message => { + let mut inner = pair.into_inner(); + Ok(Op::Message(unquote(inner.next().unwrap().as_str()))) + } Rule::filter_group => { let v: Vec<_> = pair.into_inner().map(|x| unquote(x.as_str())).collect(); @@ -137,9 +141,7 @@ fn parse_item(pair: pest::iterators::Pair) -> JoshResult { let ids = pair .into_inner() .tuples() - .map(|(oid, message)| { - Ok((LazyRef::parse(oid.as_str())?, unquote(message.as_str()))) - }) + .map(|(oid, filter)| Ok((LazyRef::parse(oid.as_str())?, parse(filter.as_str())?))) .collect::>()?; Ok(Op::Squash(Some(ids))) diff --git a/josh-core/src/history.rs b/josh-core/src/history.rs index 4fc77144..a0a339af 100644 --- a/josh-core/src/history.rs +++ b/josh-core/src/history.rs @@ -189,29 +189,43 @@ fn find_known( Ok((known, n_new)) } +pub struct RewriteData<'a> { + pub tree: git2::Tree<'a>, + pub author: Option<(String, String)>, + pub committer: Option<(String, String)>, + pub message: Option, +} + // takes everything from base except it's tree and replaces it with the tree // given pub fn rewrite_commit( repo: &git2::Repository, base: &git2::Commit, parents: &[&git2::Commit], - tree: &git2::Tree, - author: Option<(String, String)>, - message: Option, + rewrite_data: RewriteData, unsign: bool, ) -> JoshResult { - let message = message.unwrap_or(base.message_raw().unwrap_or("no message").to_string()); - - let b = if let Some((author, email)) = author { - let a = base.author(); - let new_a = git2::Signature::new(&author, &email, &a.when())?; - let c = base.committer(); - let new_c = git2::Signature::new(&author, &email, &c.when())?; - repo.commit_create_buffer(&new_a, &new_c, &message, tree, parents)? + let message = rewrite_data + .message + .unwrap_or(base.message_raw().unwrap_or("no message").to_string()); + let tree = &rewrite_data.tree; + + let a = base.author(); + let new_a = if let Some((author, email)) = rewrite_data.author { + git2::Signature::new(&author, &email, &a.when())? + } else { + a + }; + + let c = base.committer(); + let new_c = if let Some((committer, email)) = rewrite_data.committer { + git2::Signature::new(&committer, &email, &c.when())? } else { - repo.commit_create_buffer(&base.author(), &base.committer(), &message, tree, parents)? + c }; + let b = repo.commit_create_buffer(&new_a, &new_c, &message, tree, parents)?; + 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). @@ -589,9 +603,12 @@ pub fn unapply_filter( transaction.repo(), &module_commit, &original_parents, - &new_tree, - None, - None, + RewriteData { + tree: new_tree.clone(), + author: None, + committer: None, + message: None, + }, false, )?; @@ -645,9 +662,12 @@ pub fn remove_commit_signature<'a>( transaction.repo(), original_commit, filtered_parent_ids, - filtered_tree, - None, - None, + RewriteData { + tree: filtered_tree, + author: None, + committer: None, + message: None, + }, true, )?; @@ -678,19 +698,15 @@ pub fn drop_commit<'a>( pub fn create_filtered_commit<'a>( original_commit: &'a git2::Commit, filtered_parent_ids: Vec, - filtered_tree: git2::Tree<'a>, + rewrite_data: RewriteData, transaction: &cache::Transaction, filter: filter::Filter, - author: Option<(String, String)>, - message: Option, ) -> JoshResult { let (r, is_new) = create_filtered_commit2( transaction.repo(), original_commit, filtered_parent_ids, - filtered_tree, - author, - message, + rewrite_data, false, )?; @@ -705,9 +721,7 @@ fn create_filtered_commit2<'a>( repo: &'a git2::Repository, original_commit: &'a git2::Commit, filtered_parent_ids: Vec, - filtered_tree: git2::Tree<'a>, - author: Option<(String, String)>, - message: Option, + rewrite_data: RewriteData, unsign: bool, ) -> JoshResult<(git2::Oid, bool)> { let filtered_parent_commits: Result, _> = filtered_parent_ids @@ -732,7 +746,7 @@ fn create_filtered_commit2<'a>( let selected_filtered_parent_commits: Vec<&_> = select_parent_commits( original_commit, - filtered_tree.id(), + rewrite_data.tree.id(), filtered_parent_commits.iter().collect(), ); @@ -742,7 +756,7 @@ fn create_filtered_commit2<'a>( if !filtered_parent_commits.is_empty() { return Ok((filtered_parent_commits[0].id(), false)); } - if filtered_tree.id() == filter::tree::empty_id() { + if rewrite_data.tree.id() == filter::tree::empty_id() { return Ok((git2::Oid::zero(), false)); } } @@ -752,9 +766,7 @@ fn create_filtered_commit2<'a>( repo, original_commit, &selected_filtered_parent_commits, - &filtered_tree, - author, - message, + rewrite_data, unsign, )?, true, diff --git a/josh-filter/src/bin/josh-filter.rs b/josh-filter/src/bin/josh-filter.rs index e916a75b..674e810f 100644 --- a/josh-filter/src/bin/josh-filter.rs +++ b/josh-filter/src/bin/josh-filter.rs @@ -152,7 +152,7 @@ fn run_filter(args: Vec) -> josh::JoshResult { let input_ref = args.get_one::("input").unwrap(); let mut refs = vec![]; - let mut ids = vec![]; + let mut ids: Vec<(git2::Oid, josh::filter::Filter)> = vec![]; let reference = repo.resolve_reference_from_short_name(input_ref).unwrap(); let input_ref = reference.name().unwrap().to_string(); @@ -167,7 +167,7 @@ fn run_filter(args: Vec) -> josh::JoshResult { for reference in repo.references_glob(&pattern).unwrap() { let reference = reference?; let target = reference.peel_to_commit()?.id(); - ids.push((target, reference.name().unwrap().to_string())); + ids.push((target, josh::filter::message(reference.name().unwrap()))); refs.push((reference.name().unwrap().to_string(), target)); } filterobj = josh::filter::chain(josh::filter::squash(Some(&ids)), filterobj); @@ -181,7 +181,7 @@ fn run_filter(args: Vec) -> josh::JoshResult { if let [sha, name] = split.as_slice() { let target = git2::Oid::from_str(sha)?; let target = repo.find_object(target, None)?.peel_to_commit()?.id(); - ids.push((target, name.to_string())); + ids.push((target, josh::filter::message(name))); refs.push((name.to_string(), target)); } else if !split.is_empty() { eprintln!("Warning: malformed line: {:?}", line); diff --git a/josh-proxy/src/lib.rs b/josh-proxy/src/lib.rs index 6e34e29b..db68f154 100644 --- a/josh-proxy/src/lib.rs +++ b/josh-proxy/src/lib.rs @@ -6,6 +6,7 @@ pub mod trace; #[macro_use] extern crate lazy_static; +use josh::history::RewriteData; use josh::{josh_error, JoshError, JoshResult}; use std::fs; use std::path::PathBuf; @@ -459,9 +460,12 @@ fn split_changes( repo, &repo.find_commit(changes[i].1)?, &[&parent], - &new_tree, - None, - None, + RewriteData { + tree: new_tree, + author: None, + committer: None, + message: None, + }, false, )?; changes[i].1 = new_commit; diff --git a/tests/filter/squash.t b/tests/filter/squash.t index b4459793..abcf8db7 100644 --- a/tests/filter/squash.t +++ b/tests/filter/squash.t @@ -40,6 +40,7 @@ This one tag is an annotated tag, to make sure those are handled as well $ git tag -a tag_a -m "created a tag" 1d69b7d $ josh-filter -s --squash-pattern "refs/tags/*" :author=\"New\ Author\"\;\"new@e.mail\" --update refs/heads/filtered + [1] :"refs/tags/tag_a" [1] :author="New Author";"new@e.mail" [1] :squash( 1d69b7d2651f744be3416f2ad526aeccefb99310:"refs/tags/tag_a" @@ -60,6 +61,9 @@ This one tag is an annotated tag, to make sure those are handled as well * 0b4cf6c9efbbda1eada39fa9c1d21d2525b027bb (tag: tag_b) add file1 $ josh-filter -s --squash-pattern "refs/tags/*" :author=\"New\ Author\"\;\"new@e.mail\" --update refs/heads/filtered + [1] :"refs/tags/filtered/tag_a" + [1] :"refs/tags/tag_a" + [1] :"refs/tags/tag_b" [1] :squash( 1d69b7d2651f744be3416f2ad526aeccefb99310:"refs/tags/tag_a" ) @@ -114,6 +118,10 @@ This one tag is an annotated tag, to make sure those are handled as well 975d4c4975912729482cc864d321c5196a969271:"refs/tags/tag_c" ):author="New Author";"new@e.mail" $ josh-filter -s --file filter.josh --update refs/heads/filtered + [1] :"refs/tags/filtered/tag_a" + [1] :"refs/tags/tag_a" + [1] :"refs/tags/tag_b" + [1] :"refs/tags/tag_c" [1] :squash( 1d69b7d2651f744be3416f2ad526aeccefb99310:"refs/tags/tag_a" ) diff --git a/tests/filter/squash_empty_initial.t b/tests/filter/squash_empty_initial.t index 6fb2ebae..248cbf10 100644 --- a/tests/filter/squash_empty_initial.t +++ b/tests/filter/squash_empty_initial.t @@ -45,6 +45,7 @@ $ git tag -a tag_a -m "created a tag" 882f2656a5075936eb37bfefde740e0b453e4479 $ josh-filter -s --squash-pattern "refs/tags/*" :author=\"New\ Author\"\;\"new@e.mail\" --update refs/heads/filtered + [1] :"refs/tags/tag_a" [1] :author="New Author";"new@e.mail" [1] :squash( 882f2656a5075936eb37bfefde740e0b453e4479:"refs/tags/tag_a" diff --git a/tests/proxy/workspace_errors.t b/tests/proxy/workspace_errors.t index 7ce72e17..c1b4231c 100644 --- a/tests/proxy/workspace_errors.t +++ b/tests/proxy/workspace_errors.t @@ -106,7 +106,7 @@ Error in filter remote: 1 | a/b = :b/sub2 remote: | ^--- remote: | - remote: = expected EOI, filter_group, filter_subdir, filter_nop, filter_presub, filter, filter_noarg, filter_rev, filter_join, filter_replace, or filter_squash + remote: = expected EOI, filter_group, filter_subdir, filter_nop, filter_presub, filter, filter_noarg, filter_message, filter_rev, filter_join, filter_replace, or filter_squash remote: remote: a/b = :b/sub2 remote: c = :/sub1