diff --git a/docs/src/reference/filters.md b/docs/src/reference/filters.md index 2580b0617..696b42cd5 100644 --- a/docs/src/reference/filters.md +++ b/docs/src/reference/filters.md @@ -114,6 +114,11 @@ 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. +### Start filtering from a specific commit **:from(:filter)** + +Produce a history that keeps the original history leading up to the specified commit `` unchanged, +but applies the given `:filter` to all commits from that commit onwards. + ### Prune trivial merge commits **:prune=trivial-merge** Produce a history that skips all merge commits whose tree is identical to the first parents diff --git a/josh-core/src/filter/grammar.pest b/josh-core/src/filter/grammar.pest index 0a6a28bea..1f8c94839 100644 --- a/josh-core/src/filter/grammar.pest +++ b/josh-core/src/filter/grammar.pest @@ -24,6 +24,8 @@ filter_spec = { ( filter_group | filter_message | filter_rev + | filter_from + | filter_concat | filter_join | filter_replace | filter_squash @@ -51,6 +53,24 @@ filter_rev = { ~ ")" } +filter_from = { + CMD_START ~ "from" ~ "(" + ~ NEWLINE* + ~ (rev ~ filter_spec)? + ~ (CMD_SEP+ ~ (rev ~ filter_spec))* + ~ NEWLINE* + ~ ")" +} + +filter_concat = { + CMD_START ~ "from" ~ "(" + ~ NEWLINE* + ~ (rev ~ filter_spec)? + ~ (CMD_SEP+ ~ (rev ~ filter_spec))* + ~ NEWLINE* + ~ ")" +} + filter_join = { CMD_START ~ "join" ~ "(" ~ NEWLINE* @@ -60,7 +80,6 @@ filter_join = { ~ ")" } - filter_replace = { CMD_START ~ "replace" ~ "(" ~ NEWLINE* diff --git a/josh-core/src/filter/mod.rs b/josh-core/src/filter/mod.rs index 25e61d316..592aa1c5c 100644 --- a/josh-core/src/filter/mod.rs +++ b/josh-core/src/filter/mod.rs @@ -286,6 +286,8 @@ enum Op { Pattern(String), Message(String), + HistoryConcat(LazyRef, Filter), + Compose(Vec), Chain(Filter, Filter), Subtract(Filter, Filter), @@ -410,6 +412,13 @@ fn lazy_refs2(op: &Op) -> Vec { av } Op::Rev(filters) => lazy_refs2(&Op::Join(filters.clone())), + Op::HistoryConcat(r, _) => { + let mut lr = Vec::new(); + if let LazyRef::Lazy(s) = r { + lr.push(s.to_owned()); + } + lr + } Op::Join(filters) => { let mut lr = lazy_refs2(&Op::Compose(filters.values().copied().collect())); lr.extend(filters.keys().filter_map(|x| { @@ -470,6 +479,19 @@ fn resolve_refs2(refs: &std::collections::HashMap, op: &Op) - .collect(); Op::Rev(lr) } + Op::HistoryConcat(r, filter) => { + let f = resolve_refs(refs, *filter); + let resolved_ref = if let LazyRef::Lazy(s) = r { + if let Some(res) = refs.get(s) { + LazyRef::Resolved(*res) + } else { + r.clone() + } + } else { + r.clone() + }; + Op::HistoryConcat(resolved_ref, f) + } Op::Join(filters) => { let lr = filters .iter() @@ -611,6 +633,9 @@ fn spec2(op: &Op) -> String { Op::Message(m) => { format!(":{}", parse::quote(m)) } + Op::HistoryConcat(r, filter) => { + format!(":concat({}{})", r.to_string(), spec(*filter)) + } Op::Hook(hook) => { format!(":hook={}", parse::quote(hook)) } @@ -1025,6 +1050,19 @@ fn apply_to_commit2( return per_rev_filter(transaction, commit, filter, commit_filter, parent_filters); } + Op::HistoryConcat(r, f) => { + if let LazyRef::Resolved(c) = r { + let a = apply_to_commit2(&to_op(*f), &repo.find_commit(*c)?, transaction)?; + let a = some_or!(a, { return Ok(None) }); + if commit.id() == a { + transaction.insert(filter, commit.id(), *c, true); + return Ok(Some(*c)); + } + } else { + return Err(josh_error("unresolved lazy ref")); + } + Apply::from_commit(commit)? + } _ => apply(transaction, filter, Apply::from_commit(commit)?)?, }; @@ -1065,7 +1103,7 @@ fn apply2<'a>(transaction: &'a cache::Transaction, op: &Op, x: Apply<'a>) -> Jos Op::Nop => Ok(x), Op::Empty => Ok(x.with_tree(tree::empty(repo))), Op::Fold => Ok(x), - Op::Squash(None) => Ok(x), + Op::Squash(..) => Ok(x), Op::Author(author, email) => Ok(x.with_author((author.clone(), email.clone()))), Op::Committer(author, email) => Ok(x.with_committer((author.clone(), email.clone()))), Op::Message(m) => Ok(x.with_message( @@ -1075,7 +1113,7 @@ fn apply2<'a>(transaction: &'a cache::Transaction, op: &Op, x: Apply<'a>) -> Jos &std::collections::HashMap::::new(), )?, )), - Op::Squash(Some(_)) => Err(josh_error("not applicable to tree")), + Op::HistoryConcat(..) => Ok(x), Op::Linear => Ok(x), Op::Prune => Ok(x), Op::Unsign => Ok(x), diff --git a/josh-core/src/filter/parse.rs b/josh-core/src/filter/parse.rs index 3709b94a9..4c9b7b9ab 100644 --- a/josh-core/src/filter/parse.rs +++ b/josh-core/src/filter/parse.rs @@ -146,6 +146,31 @@ fn parse_item(pair: pest::iterators::Pair) -> JoshResult { Ok(Op::Rev(hm)) } + Rule::filter_from => { + let v: Vec<_> = pair.into_inner().map(|x| x.as_str()).collect(); + + if v.len() == 2 { + let oid = LazyRef::parse(v[0])?; + let filter = parse(v[1])?; + Ok(Op::Chain( + filter, + filter::to_filter(Op::HistoryConcat(oid, filter)), + )) + } else { + Err(josh_error("wrong argument count for :from")) + } + } + Rule::filter_concat => { + let v: Vec<_> = pair.into_inner().map(|x| x.as_str()).collect(); + + if v.len() == 2 { + let oid = LazyRef::parse(v[0])?; + let filter = parse(v[1])?; + Ok(Op::HistoryConcat(oid, filter)) + } else { + Err(josh_error("wrong argument count for :concat")) + } + } Rule::filter_replace => { let replacements = pair .into_inner() diff --git a/josh-core/src/filter/persist.rs b/josh-core/src/filter/persist.rs index 1f8796788..97545f948 100644 --- a/josh-core/src/filter/persist.rs +++ b/josh-core/src/filter/persist.rs @@ -275,6 +275,10 @@ impl InMemoryBuilder { let params_tree = self.build_rev_params(&v)?; push_tree_entries(&mut entries, [("join", params_tree)]); } + Op::HistoryConcat(lr, f) => { + let params_tree = self.build_rev_params(&[(lr.to_string(), *f)])?; + push_tree_entries(&mut entries, [("concat", params_tree)]); + } Op::Squash(Some(ids)) => { let mut v = ids .iter() @@ -572,6 +576,28 @@ fn from_tree2(repo: &git2::Repository, tree_oid: git2::Oid) -> JoshResult { } Ok(Op::Join(filters)) } + "concat" => { + let concat_tree = repo.find_tree(entry.id())?; + let entry = concat_tree + .get(0) + .ok_or_else(|| josh_error("concat: missing entry"))?; + let inner_tree = repo.find_tree(entry.id())?; + let key_blob = repo.find_blob( + inner_tree + .get_name("o") + .ok_or_else(|| josh_error("concat: missing key"))? + .id(), + )?; + let filter_tree = repo.find_tree( + inner_tree + .get_name("f") + .ok_or_else(|| josh_error("concat: missing filter"))? + .id(), + )?; + let key = std::str::from_utf8(key_blob.content())?.to_string(); + let filter = from_tree2(repo, filter_tree.id())?; + Ok(Op::HistoryConcat(LazyRef::parse(&key)?, to_filter(filter))) + } "squash" => { // blob -> Squash(None), tree -> Squash(Some(...)) if let Some(kind) = entry.kind() { diff --git a/tests/filter/concat.t b/tests/filter/concat.t new file mode 100644 index 000000000..05dab45d5 --- /dev/null +++ b/tests/filter/concat.t @@ -0,0 +1,42 @@ + $ export TESTTMP=${PWD} + + $ cd ${TESTTMP} + $ git init -q libs 1> /dev/null + $ cd libs + + $ mkdir sub1 + $ echo contents1 > sub1/file1 + $ git add sub1 + $ git commit -m "add file1" 1> /dev/null + + $ echo contents2 > sub1/file2 + $ git add sub1 + $ git commit -m "add file2" 1> /dev/null + $ git update-ref refs/heads/from_here HEAD + + + $ mkdir sub2 + $ echo contents1 > sub2/file3 + $ git add sub2 + $ git commit -m "add file3" 1> /dev/null + + $ josh-filter ":\"x\"" + + $ git log --graph --pretty=%s:%H HEAD + * add file3:667a912db7482f3c8023082c9b4c7b267792633a + * add file2:81b10fb4984d20142cd275b89c91c346e536876a + * add file1:bb282e9cdc1b972fffd08fd21eead43bc0c83cb8 + + $ git log --graph --pretty=%s:%H FILTERED_HEAD + * x:9d117d96dfdba145df43ebe37d9e526acac4b17c + * x:b232aa8eefaadfb5e38b3ad7355118aa59fb651e + * x:6b4d1f87c2be08f7d0f9d40b6679aab612e259b1 + + $ josh-filter -p ":from(81b10fb4984d20142cd275b89c91c346e536876a:\"x\")" + :"x":concat(81b10fb4984d20142cd275b89c91c346e536876a:"x") + $ josh-filter ":from(81b10fb4984d20142cd275b89c91c346e536876a:\"x\")" + + $ git log --graph --pretty=%s FILTERED_HEAD + * x + * add file2 + * add file1 diff --git a/tests/proxy/workspace_errors.t b/tests/proxy/workspace_errors.t index c1b4231ce..7918fa6e7 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_message, 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_from, filter_concat, filter_join, filter_replace, or filter_squash remote: remote: a/b = :b/sub2 remote: c = :/sub1