Skip to content

Commit c8084be

Browse files
Add :at_commit=sha[...] filter
Change: start-filter
1 parent af0efe1 commit c8084be

File tree

8 files changed

+214
-3
lines changed

8 files changed

+214
-3
lines changed

docs/src/reference/filters.md

+7
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,13 @@ These filter do not modify git trees, but instead only operate on the commit gra
9191
Produce a filtered history that does not contain any merge commits. This is done by
9292
simply dropping all parents except the first on every commit.
9393

94+
### Filter specific part of the history **:at_commit=<sha>[:filter]**
95+
Produce a history where the commit specified by `<sha>` is replaced by the result of applying
96+
`:filter` to it.
97+
This means also all parents of this specific commit appear filtered with `:filter` and all
98+
descendent commits will be left unchanged. However all commits hashes will still be different
99+
due to the filtered parents.
100+
94101
Filter order matters
95102
--------------------
96103

src/filter/grammar.pest

+2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ char = {
1818

1919
filter_spec = { (
2020
filter_group
21+
| filter_group_arg
2122
| filter_presub
2223
| filter_subdir
2324
| filter_nop
@@ -26,6 +27,7 @@ filter_spec = { (
2627
)+ }
2728

2829
filter_group = { CMD_START ~ cmd? ~ GROUP_START ~ compose ~ GROUP_END }
30+
filter_group_arg = { CMD_START ~ cmd ~ "=" ~ argument ~ GROUP_START ~ compose ~ GROUP_END }
2931
filter_subdir = { CMD_START ~ "/" ~ argument }
3032
filter_nop = { CMD_START ~ "/" }
3133
filter_presub = { CMD_START ~ ":" ~ argument }

src/filter/mod.rs

+25-2
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ enum Op {
9898
Fold,
9999
Paths,
100100
Squash(Option<std::collections::HashMap<git2::Oid, (String, String, String)>>),
101+
AtCommit(git2::Oid, Filter),
101102
Linear,
102103

103104
RegexReplace(regex::Regex, String),
@@ -194,6 +195,7 @@ fn nesting2(op: &Op) -> usize {
194195
Op::Workspace(_) => usize::MAX,
195196
Op::Chain(a, b) => 1 + nesting(*a).max(nesting(*b)),
196197
Op::Subtract(a, b) => 1 + nesting(*a).max(nesting(*b)),
198+
Op::AtCommit(_, filter) => 1 + nesting(*filter),
197199
_ => 0,
198200
}
199201
}
@@ -223,6 +225,9 @@ fn spec2(op: &Op) -> String {
223225
Op::Exclude(b) => {
224226
format!(":exclude[{}]", spec(*b))
225227
}
228+
Op::AtCommit(id, b) => {
229+
format!(":at_commit={}[{}]", id, spec(*b))
230+
}
226231
Op::Workspace(path) => {
227232
format!(":workspace={}", parse::quote(&path.to_string_lossy()))
228233
}
@@ -384,6 +389,18 @@ fn apply_to_commit2(
384389
rs_tracing::trace_scoped!("apply_to_commit", "spec": spec(filter), "commit": commit.id().to_string());
385390

386391
let filtered_tree = match &to_op(filter) {
392+
Op::AtCommit(id, startfilter) => {
393+
if *id == commit.id() {
394+
if let Some(start) = apply_to_commit2(&to_op(*startfilter), &commit, transaction)? {
395+
transaction.insert(filter, commit.id(), start, true);
396+
return Ok(Some(start));
397+
} else {
398+
return Ok(None);
399+
}
400+
} else {
401+
commit.tree()?
402+
}
403+
}
387404
Op::Squash(Some(ids)) => {
388405
if let Some(_) = ids.get(&commit.id()) {
389406
commit.tree()?
@@ -619,6 +636,7 @@ fn apply2<'a>(
619636
Op::Squash(None) => Ok(tree),
620637
Op::Squash(Some(_)) => Err(josh_error("not applicable to tree")),
621638
Op::Linear => Ok(tree),
639+
Op::AtCommit(_, _) => Err(josh_error("not applicable to tree")),
622640

623641
Op::RegexReplace(regex, replacement) => {
624642
tree::regex_replace(tree.id(), &regex, &replacement, transaction)
@@ -712,7 +730,11 @@ pub fn unapply<'a>(
712730
parent_tree: git2::Tree<'a>,
713731
) -> JoshResult<git2::Tree<'a>> {
714732
if let Ok(inverted) = invert(filter) {
715-
let matching = apply(transaction, chain(filter, inverted), parent_tree.clone())?;
733+
let matching = apply(
734+
transaction,
735+
chain(invert(inverted)?, inverted),
736+
parent_tree.clone(),
737+
)?;
716738
let stripped = tree::subtract(transaction, parent_tree.id(), matching.id())?;
717739
let new_tree = apply(transaction, inverted, tree)?;
718740

@@ -733,7 +755,8 @@ pub fn unapply<'a>(
733755
}
734756

735757
if let Op::Chain(a, b) = to_op(filter) {
736-
let p = apply(transaction, a, parent_tree.clone())?;
758+
let i = if let Ok(i) = invert(a) { invert(i)? } else { a };
759+
let p = apply(transaction, i, parent_tree.clone())?;
737760
return unapply(
738761
transaction,
739762
a,

src/filter/opt.rs

+2
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,7 @@ fn step(filter: Filter) -> Filter {
328328
Op::Prefix(path)
329329
}
330330
}
331+
Op::AtCommit(id, filter) => Op::AtCommit(id, step(filter)),
331332
Op::Compose(filters) if filters.is_empty() => Op::Empty,
332333
Op::Compose(filters) if filters.len() == 1 => to_op(filters[0]),
333334
Op::Compose(mut filters) => {
@@ -419,6 +420,7 @@ pub fn invert(filter: Filter) -> JoshResult<Filter> {
419420
Op::File(path) => Some(Op::File(path)),
420421
Op::Prefix(path) => Some(Op::Subdir(path)),
421422
Op::Glob(pattern) => Some(Op::Glob(pattern)),
423+
Op::AtCommit(_, _) => Some(Op::Nop),
422424
_ => None,
423425
};
424426

src/filter/parse.rs

+17
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,23 @@ fn parse_item(pair: pest::iterators::Pair<Rule>) -> JoshResult<Op> {
103103
_ => Err(josh_error("parse_item: no match {:?}")),
104104
}
105105
}
106+
Rule::filter_group_arg => {
107+
let v: Vec<_> = pair.into_inner().map(|x| unquote(x.as_str())).collect();
108+
109+
match v.as_slice() {
110+
[cmd, arg, args] => {
111+
let g = parse_group(args)?;
112+
match *cmd {
113+
"at_commit" => Ok(Op::AtCommit(
114+
git2::Oid::from_str(arg)?,
115+
to_filter(Op::Compose(g)),
116+
)),
117+
_ => Err(josh_error("parse_item: no match")),
118+
}
119+
}
120+
_ => Err(josh_error("parse_item: no match {:?}")),
121+
}
122+
}
106123
_ => Err(josh_error("parse_item: no match")),
107124
}
108125
}

tests/filter/start.t

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
$ export RUST_BACKTRACE=1
2+
$ git init -q 1> /dev/null
3+
4+
$ echo contents1 > file1
5+
$ git add .
6+
$ git commit -m "add file1" 1> /dev/null
7+
8+
$ git log --graph --pretty=%s
9+
* add file1
10+
11+
$ git checkout -b branch2
12+
Switched to a new branch 'branch2'
13+
14+
$ echo contents2 > file1
15+
$ git add .
16+
$ git commit -m "mod file1" 1> /dev/null
17+
18+
$ echo contents3 > file3
19+
$ git add .
20+
$ git commit -m "mod file3" 1> /dev/null
21+
22+
$ git checkout master
23+
Switched to branch 'master'
24+
25+
$ echo contents3 > file2
26+
$ git add .
27+
$ git commit -m "add file2" 1> /dev/null
28+
29+
$ git merge -q branch2 --no-ff
30+
31+
$ git log --graph --pretty=%H
32+
* 1d69b7d2651f744be3416f2ad526aeccefb99310
33+
|\
34+
| * 86871b8775ad3baca86484337d1072aa1d386f7e
35+
| * 975d4c4975912729482cc864d321c5196a969271
36+
* | e707f76bb6a1390f28b2162da5b5eb6933009070
37+
|/
38+
* 0b4cf6c9efbbda1eada39fa9c1d21d2525b027bb
39+
40+
$ josh-filter -s :at_commit=975d4c4975912729482cc864d321c5196a969271[:prefix=x/y] --update refs/heads/filtered
41+
[2] :prefix=x
42+
[2] :prefix=y
43+
[5] :at_commit=975d4c4975912729482cc864d321c5196a969271[:prefix=x/y]
44+
45+
$ git log --graph --decorate --pretty=%H refs/heads/filtered
46+
* 8b4097f3318cdf47e46266fc7fef5331bf189b6c
47+
|\
48+
| * ee931ac07e4a953d1d2e0f65968946f5c09b0f4c
49+
| * cc0382917c6488d69dca4d6a147d55251b06ac08
50+
| * 9f0db868b59a422c114df33bc6a8b2950f80490b
51+
* e707f76bb6a1390f28b2162da5b5eb6933009070
52+
* 0b4cf6c9efbbda1eada39fa9c1d21d2525b027bb

tests/filter/subtree_prefix.t

+108
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
$ git init -q 1>/dev/null
2+
3+
Initial commit of main branch
4+
$ echo contents1 > file1
5+
$ git add .
6+
$ git commit -m "add file1" 1>/dev/null
7+
8+
Initial commit of subtree branch
9+
$ git checkout --orphan subtree
10+
Switched to a new branch 'subtree'
11+
$ rm file*
12+
$ echo contents2 > file2
13+
$ git add .
14+
$ git commit -m "add file2 (in subtree)" 1>/dev/null
15+
$ export SUBTREE_TIP=$(git rev-parse HEAD)
16+
17+
Articially create a subtree merge
18+
(merge commit has subtree files in subfolder but has subtree commit as a parent)
19+
$ git checkout master
20+
Switched to branch 'master'
21+
$ git merge subtree --allow-unrelated-histories 1>/dev/null
22+
$ mkdir subtree
23+
$ git mv file2 subtree/
24+
$ git add subtree
25+
$ git commit -a --amend -m "subtree merge" 1>/dev/null
26+
$ tree
27+
.
28+
|-- file1
29+
`-- subtree
30+
`-- file2
31+
32+
1 directory, 2 files
33+
$ git log --graph --pretty=%s
34+
* subtree merge
35+
|\
36+
| * add file2 (in subtree)
37+
* add file1
38+
39+
Change subtree file
40+
$ echo more contents >> subtree/file2
41+
$ git commit -a -m "subtree edit from main repo" 1>/dev/null
42+
43+
Rewrite the subtree part of the history
44+
$ josh-filter -s :at_commit=$SUBTREE_TIP[:prefix=subtree] refs/heads/master --update refs/heads/filtered
45+
[1] :prefix=subtree
46+
[4] :at_commit=c036f944faafb865e0585e4fa5e005afa0aeea3f[:prefix=subtree]
47+
48+
$ git log --graph --pretty=%s refs/heads/filtered
49+
* subtree edit from main repo
50+
* subtree merge
51+
|\
52+
| * add file2 (in subtree)
53+
* add file1
54+
55+
Compare input and result. ^^2 is the 2nd parent of the first parent, i.e., the 'in subtree' commit.
56+
$ git ls-tree --name-only -r refs/heads/filtered
57+
file1
58+
subtree/file2
59+
$ git diff refs/heads/master refs/heads/filtered
60+
$ git ls-tree --name-only -r refs/heads/filtered^^2
61+
subtree/file2
62+
$ git diff refs/heads/master^^2 refs/heads/filtered^^2
63+
diff --git a/file2 b/subtree/file2
64+
similarity index 100%
65+
rename from file2
66+
rename to subtree/file2
67+
68+
Extract the subtree history
69+
$ josh-filter -s :at_commit=$SUBTREE_TIP[:prefix=subtree]:/subtree refs/heads/master --update refs/heads/subtree
70+
[1] :prefix=subtree
71+
[4] :/subtree
72+
[4] :at_commit=c036f944faafb865e0585e4fa5e005afa0aeea3f[:prefix=subtree]
73+
$ git checkout subtree
74+
Switched to branch 'subtree'
75+
$ cat file2
76+
contents2
77+
more contents
78+
79+
Work in the subtree, and sync that back.
80+
$ echo even more contents >> file2
81+
$ git commit -am "add even more content" 1>/dev/null
82+
$ josh-filter -s :at_commit=$SUBTREE_TIP[:prefix=subtree]:/subtree refs/heads/master --update refs/heads/subtree --reverse
83+
[1] :prefix=subtree
84+
[4] :/subtree
85+
[4] :at_commit=c036f944faafb865e0585e4fa5e005afa0aeea3f[:prefix=subtree]
86+
$ git log --graph --pretty=%s refs/heads/master
87+
* add even more content
88+
* subtree edit from main repo
89+
* subtree merge
90+
|\
91+
| * add file2 (in subtree)
92+
* add file1
93+
$ git ls-tree --name-only -r refs/heads/master
94+
file1
95+
subtree/file2
96+
$ git checkout master
97+
Switched to branch 'master'
98+
$ cat subtree/file2
99+
contents2
100+
more contents
101+
even more contents
102+
103+
And then re-extract, which should re-construct the same subtree.
104+
$ josh-filter -s :at_commit=$SUBTREE_TIP[:prefix=subtree]:/subtree refs/heads/master --update refs/heads/subtree2
105+
[1] :prefix=subtree
106+
[5] :/subtree
107+
[5] :at_commit=c036f944faafb865e0585e4fa5e005afa0aeea3f[:prefix=subtree]
108+
$ test $(git rev-parse subtree) = $(git rev-parse subtree2)

tests/proxy/workspace_errors.t

+1-1
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ Error in filter
104104
remote: 1 | a/b = :b/sub2
105105
remote: | ^---
106106
remote: |
107-
remote: = expected EOI, filter_group, filter_subdir, filter_nop, filter_presub, filter, or filter_noarg
107+
remote: = expected EOI, filter_group, filter_group_arg, filter_subdir, filter_nop, filter_presub, filter, or filter_noarg
108108
remote:
109109
remote: a/b = :b/sub2
110110
remote: c = :/sub1

0 commit comments

Comments
 (0)