Skip to content

Commit 7e1962c

Browse files
Implement :from filter
Change: from-filter
1 parent 170fb37 commit 7e1962c

File tree

10 files changed

+613
-6
lines changed

10 files changed

+613
-6
lines changed

docs/src/reference/filters.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,13 +114,34 @@ commits that don't match any of the other shas.
114114
Produce the history that would be the result of pushing the passed branches with the
115115
passed filters into the upstream.
116116

117+
### Start filtering from a specific commit **:from(<sha>:filter)**
118+
119+
Produce a history that keeps the original history leading up to the specified commit `<sha>` unchanged,
120+
but applies the given `:filter` to all commits from that commit onwards.
121+
117122
### Prune trivial merge commits **:prune=trivial-merge**
118123

119124
Produce a history that skips all merge commits whose tree is identical to the first parents
120125
tree.
121126
Normally Josh will keep all commits in the filtered history whose tree differs from any of it's
122127
parents.
123128

129+
### Pin tree contents
130+
131+
`:pin` filter prevents appearance of selected subtrees for a given revision.
132+
133+
In practical terms, it means that file and folder updates are "held off", and revisions are "pinned".
134+
If a tree entry already existed in the parent revision, that version will be chosen.
135+
Otherwise, the tree entry will not appear in the filtered commit.
136+
137+
The source of the parent revision is always the first commit parent.
138+
139+
Note that this filter is only practical when used with `:hook` or `workspace.josh`,
140+
as it should apply per-revision only. Applying `:pin` for the whole history
141+
will result in the subtree being excluded from all revisions.
142+
143+
Refer to `pin_filter_workspace.t` and `pin_filter_hook.t` for reference.
144+
124145
Filter order matters
125146
--------------------
126147

josh-core/src/filter/grammar.pest

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ filter_spec = { (
2424
filter_group
2525
| filter_message
2626
| filter_rev
27+
| filter_from
28+
| filter_concat
2729
| filter_join
2830
| filter_replace
2931
| filter_squash
@@ -51,6 +53,24 @@ filter_rev = {
5153
~ ")"
5254
}
5355

56+
filter_from = {
57+
CMD_START ~ "from" ~ "("
58+
~ NEWLINE*
59+
~ (rev ~ filter_spec)?
60+
~ (CMD_SEP+ ~ (rev ~ filter_spec))*
61+
~ NEWLINE*
62+
~ ")"
63+
}
64+
65+
filter_concat = {
66+
CMD_START ~ "from" ~ "("
67+
~ NEWLINE*
68+
~ (rev ~ filter_spec)?
69+
~ (CMD_SEP+ ~ (rev ~ filter_spec))*
70+
~ NEWLINE*
71+
~ ")"
72+
}
73+
5474
filter_join = {
5575
CMD_START ~ "join" ~ "("
5676
~ NEWLINE*
@@ -60,7 +80,6 @@ filter_join = {
6080
~ ")"
6181
}
6282

63-
6483
filter_replace = {
6584
CMD_START ~ "replace" ~ "("
6685
~ NEWLINE*

josh-core/src/filter/mod.rs

Lines changed: 84 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -300,10 +300,13 @@ enum Op {
300300
Pattern(String),
301301
Message(String),
302302

303+
HistoryConcat(LazyRef, Filter),
304+
303305
Compose(Vec<Filter>),
304306
Chain(Filter, Filter),
305307
Subtract(Filter, Filter),
306308
Exclude(Filter),
309+
Pin(Filter),
307310
}
308311

309312
/// Pretty print the filter on multiple lines with initial indentation level.
@@ -349,6 +352,10 @@ fn pretty2(op: &Op, indent: usize, compose: bool) -> String {
349352
Op::Compose(filters) => ff(&filters, "exclude", indent),
350353
b => format!(":exclude[{}]", pretty2(&b, indent, false)),
351354
},
355+
Op::Pin(filter) => match to_op(*filter) {
356+
Op::Compose(filters) => ff(&filters, "pin", indent),
357+
b => format!(":pin[{}]", pretty2(&b, indent, false)),
358+
},
352359
Op::Chain(a, b) => match (to_op(*a), to_op(*b)) {
353360
(Op::Subdir(p1), Op::Prefix(p2)) if p1 == p2 => {
354361
format!("::{}/", parse::quote_if(&p1.to_string_lossy()))
@@ -407,7 +414,7 @@ fn lazy_refs2(op: &Op) -> Vec<String> {
407414
acc
408415
})
409416
}
410-
Op::Exclude(filter) => lazy_refs(*filter),
417+
Op::Exclude(filter) | Op::Pin(filter) => lazy_refs(*filter),
411418
Op::Chain(a, b) => {
412419
let mut av = lazy_refs(*a);
413420
av.append(&mut lazy_refs(*b));
@@ -419,6 +426,13 @@ fn lazy_refs2(op: &Op) -> Vec<String> {
419426
av
420427
}
421428
Op::Rev(filters) => lazy_refs2(&Op::Join(filters.clone())),
429+
Op::HistoryConcat(r, _) => {
430+
let mut lr = Vec::new();
431+
if let LazyRef::Lazy(s) = r {
432+
lr.push(s.to_owned());
433+
}
434+
lr
435+
}
422436
Op::Join(filters) => {
423437
let mut lr = lazy_refs2(&Op::Compose(filters.values().copied().collect()));
424438
lr.extend(filters.keys().filter_map(|x| {
@@ -458,6 +472,7 @@ fn resolve_refs2(refs: &std::collections::HashMap<String, git2::Oid>, op: &Op) -
458472
Op::Compose(filters.iter().map(|f| resolve_refs(refs, *f)).collect())
459473
}
460474
Op::Exclude(filter) => Op::Exclude(resolve_refs(refs, *filter)),
475+
Op::Pin(filter) => Op::Pin(resolve_refs(refs, *filter)),
461476
Op::Chain(a, b) => Op::Chain(resolve_refs(refs, *a), resolve_refs(refs, *b)),
462477
Op::Subtract(a, b) => Op::Subtract(resolve_refs(refs, *a), resolve_refs(refs, *b)),
463478
Op::Rev(filters) => {
@@ -478,6 +493,19 @@ fn resolve_refs2(refs: &std::collections::HashMap<String, git2::Oid>, op: &Op) -
478493
.collect();
479494
Op::Rev(lr)
480495
}
496+
Op::HistoryConcat(r, filter) => {
497+
let f = resolve_refs(refs, *filter);
498+
let resolved_ref = if let LazyRef::Lazy(s) = r {
499+
if let Some(res) = refs.get(s) {
500+
LazyRef::Resolved(*res)
501+
} else {
502+
r.clone()
503+
}
504+
} else {
505+
r.clone()
506+
};
507+
Op::HistoryConcat(resolved_ref, f)
508+
}
481509
Op::Join(filters) => {
482510
let lr = filters
483511
.iter()
@@ -545,6 +573,9 @@ fn spec2(op: &Op) -> String {
545573
Op::Exclude(b) => {
546574
format!(":exclude[{}]", spec(*b))
547575
}
576+
Op::Pin(filter) => {
577+
format!(":pin[{}]", spec(*filter))
578+
}
548579
Op::Rev(filters) => {
549580
let mut v = filters
550581
.iter()
@@ -616,6 +647,9 @@ fn spec2(op: &Op) -> String {
616647
Op::Message(m) => {
617648
format!(":{}", parse::quote(m))
618649
}
650+
Op::HistoryConcat(r, filter) => {
651+
format!(":concat({}{})", r.to_string(), spec(*filter))
652+
}
619653
Op::Hook(hook) => {
620654
format!(":hook={}", parse::quote(hook))
621655
}
@@ -688,6 +722,9 @@ fn as_tree2(repo: &git2::Repository, op: &Op) -> JoshResult<git2::Oid> {
688722
Op::Exclude(b) => {
689723
builder.insert("exclude", as_tree(repo, *b)?, git2::FileMode::Tree.into())?;
690724
}
725+
Op::Pin(b) => {
726+
builder.insert("pin", as_tree(repo, *b)?, git2::FileMode::Tree.into())?;
727+
}
691728
Op::Subdir(path) => {
692729
builder.insert(
693730
"subdir",
@@ -808,6 +845,13 @@ fn as_tree2(repo: &git2::Repository, op: &Op) -> JoshResult<git2::Oid> {
808845
v.sort();
809846
builder.insert("rev", rev_params(repo, &v)?, git2::FileMode::Tree.into())?;
810847
}
848+
Op::HistoryConcat(r, f) => {
849+
builder.insert(
850+
"historyconcat",
851+
rev_params(repo, &vec![(r.to_string(), *f)])?,
852+
git2::FileMode::Tree.into(),
853+
)?;
854+
}
811855
Op::Join(filters) => {
812856
let mut v = filters
813857
.iter()
@@ -1064,6 +1108,11 @@ fn from_tree2(repo: &git2::Repository, tree_oid: git2::Oid) -> JoshResult<Op> {
10641108
let filter = from_tree2(repo, exclude_tree.id())?;
10651109
Ok(Op::Exclude(to_filter(filter)))
10661110
}
1111+
"pin" => {
1112+
let pin_tree = repo.find_tree(entry.id())?;
1113+
let filter = from_tree2(repo, pin_tree.id())?;
1114+
Ok(Op::Pin(to_filter(filter)))
1115+
}
10671116
"rev" => {
10681117
let rev_tree = repo.find_tree(entry.id())?;
10691118
let mut filters = std::collections::BTreeMap::new();
@@ -1608,6 +1657,19 @@ fn apply_to_commit2(
16081657
parent_filters,
16091658
);
16101659
}
1660+
Op::HistoryConcat(r, f) => {
1661+
if let LazyRef::Resolved(c) = r {
1662+
let a = apply_to_commit2(&to_op(*f), &repo.find_commit(*c)?, transaction)?;
1663+
let a = some_or!(a, { return Ok(None) });
1664+
if commit.id() == a {
1665+
transaction.insert(filter, commit.id(), *c, true);
1666+
return Ok(Some(*c));
1667+
}
1668+
} else {
1669+
return Err(josh_error("unresolved lazy ref"));
1670+
}
1671+
Apply::from_commit(commit)?
1672+
}
16111673
_ => {
16121674
let filtered_parent_ids = commit
16131675
.parent_ids()
@@ -1659,7 +1721,7 @@ fn apply2<'a>(transaction: &'a cache::Transaction, op: &Op, x: Apply<'a>) -> Jos
16591721
Op::Nop => Ok(x),
16601722
Op::Empty => Ok(x.with_tree(tree::empty(repo))),
16611723
Op::Fold => Ok(x),
1662-
Op::Squash(None) => Ok(x),
1724+
Op::Squash(..) => Ok(x),
16631725
Op::Author(author, email) => Ok(x.with_author((author.clone(), email.clone()))),
16641726
Op::Committer(author, email) => Ok(x.with_committer((author.clone(), email.clone()))),
16651727
Op::Message(m) => Ok(x.with_message(
@@ -1669,7 +1731,7 @@ fn apply2<'a>(transaction: &'a cache::Transaction, op: &Op, x: Apply<'a>) -> Jos
16691731
&std::collections::HashMap::<String, &dyn strfmt::DisplayStr>::new(),
16701732
)?,
16711733
)),
1672-
Op::Squash(Some(_)) => Err(josh_error("not applicable to tree")),
1734+
Op::HistoryConcat(..) => Ok(x),
16731735
Op::Linear => Ok(x),
16741736
Op::Prune => Ok(x),
16751737
Op::Unsign => Ok(x),
@@ -1784,6 +1846,25 @@ fn apply2<'a>(transaction: &'a cache::Transaction, op: &Op, x: Apply<'a>) -> Jos
17841846
return apply(transaction, *b, apply(transaction, *a, x.clone())?);
17851847
}
17861848
Op::Hook(_) => Err(josh_error("not applicable to tree")),
1849+
1850+
Op::Pin(pin_filter) => {
1851+
let filtered_parent = if let Some(parent) = x.parents.as_ref().and_then(|p| p.first()) {
1852+
let parent = repo.find_commit(*parent)?;
1853+
let filtered = apply(transaction, *pin_filter, Apply::from_commit(&parent)?)?;
1854+
filtered.tree.id()
1855+
} else {
1856+
tree::empty_id()
1857+
};
1858+
1859+
// Mask out all the "pinned" files from current tree
1860+
let exclude = to_filter(Op::Exclude(*pin_filter));
1861+
let with_mask = apply(transaction, exclude, x.clone())?;
1862+
1863+
// Overlay filtered parent tree on current one to override versions
1864+
let with_overlay = tree::overlay(transaction, with_mask.tree.id(), filtered_parent)?;
1865+
1866+
Ok(x.with_tree(repo.find_tree(with_overlay)?))
1867+
}
17871868
}
17881869
}
17891870

josh-core/src/filter/opt.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ pub fn simplify(filter: Filter) -> Filter {
8282
Op::Subtract(simplify(to_filter(a)), simplify(to_filter(b)))
8383
}
8484
Op::Exclude(b) => Op::Exclude(simplify(b)),
85+
Op::Pin(b) => Op::Pin(simplify(b)),
8586
_ => to_op(filter),
8687
});
8788

@@ -137,6 +138,7 @@ pub fn flatten(filter: Filter) -> Filter {
137138
Op::Subtract(flatten(to_filter(a)), flatten(to_filter(b)))
138139
}
139140
Op::Exclude(b) => Op::Exclude(flatten(b)),
141+
Op::Pin(b) => Op::Pin(flatten(b)),
140142
_ => to_op(filter),
141143
});
142144

@@ -440,8 +442,9 @@ fn step(filter: Filter) -> Filter {
440442
(a, b) => Op::Chain(step(to_filter(a)), step(to_filter(b))),
441443
},
442444
Op::Exclude(b) if b == to_filter(Op::Nop) => Op::Empty,
443-
Op::Exclude(b) if b == to_filter(Op::Empty) => Op::Nop,
445+
Op::Exclude(b) | Op::Pin(b) if b == to_filter(Op::Empty) => Op::Nop,
444446
Op::Exclude(b) => Op::Exclude(step(b)),
447+
Op::Pin(b) => Op::Pin(step(b)),
445448
Op::Subtract(a, b) if a == b => Op::Empty,
446449
Op::Subtract(af, bf) => match (to_op(af), to_op(bf)) {
447450
(Op::Empty, _) => Op::Empty,
@@ -504,6 +507,7 @@ pub fn invert(filter: Filter) -> JoshResult<Filter> {
504507
Op::Pattern(pattern) => Some(Op::Pattern(pattern)),
505508
Op::Rev(_) => Some(Op::Nop),
506509
Op::RegexReplace(_) => Some(Op::Nop),
510+
Op::Pin(_) => Some(Op::Nop),
507511
_ => None,
508512
};
509513

josh-core/src/filter/parse.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ fn parse_item(pair: pest::iterators::Pair<Rule>) -> JoshResult<Op> {
115115
[cmd, args] => {
116116
let g = parse_group(args)?;
117117
match *cmd {
118+
"pin" => Ok(Op::Pin(to_filter(Op::Compose(g)))),
118119
"exclude" => Ok(Op::Exclude(to_filter(Op::Compose(g)))),
119120
"subtract" if g.len() == 2 => Ok(Op::Subtract(g[0], g[1])),
120121
_ => Err(josh_error(&format!("parse_item: no match {:?}", cmd))),
@@ -145,6 +146,31 @@ fn parse_item(pair: pest::iterators::Pair<Rule>) -> JoshResult<Op> {
145146

146147
Ok(Op::Rev(hm))
147148
}
149+
Rule::filter_from => {
150+
let v: Vec<_> = pair.into_inner().map(|x| x.as_str()).collect();
151+
152+
if v.len() == 2 {
153+
let oid = LazyRef::parse(v[0])?;
154+
let filter = parse(v[1])?;
155+
Ok(Op::Chain(
156+
filter,
157+
filter::to_filter(Op::HistoryConcat(oid, filter)),
158+
))
159+
} else {
160+
Err(josh_error("wrong argument count for :from"))
161+
}
162+
}
163+
Rule::filter_concat => {
164+
let v: Vec<_> = pair.into_inner().map(|x| x.as_str()).collect();
165+
166+
if v.len() == 2 {
167+
let oid = LazyRef::parse(v[0])?;
168+
let filter = parse(v[1])?;
169+
Ok(Op::HistoryConcat(oid, filter))
170+
} else {
171+
Err(josh_error("wrong argument count for :concat"))
172+
}
173+
}
148174
Rule::filter_replace => {
149175
let replacements = pair
150176
.into_inner()

tests/filter/concat.t

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
$ export TESTTMP=${PWD}
2+
3+
$ cd ${TESTTMP}
4+
$ git init -q libs 1> /dev/null
5+
$ cd libs
6+
7+
$ mkdir sub1
8+
$ echo contents1 > sub1/file1
9+
$ git add sub1
10+
$ git commit -m "add file1" 1> /dev/null
11+
12+
$ echo contents2 > sub1/file2
13+
$ git add sub1
14+
$ git commit -m "add file2" 1> /dev/null
15+
$ git update-ref refs/heads/from_here HEAD
16+
17+
18+
$ mkdir sub2
19+
$ echo contents1 > sub2/file3
20+
$ git add sub2
21+
$ git commit -m "add file3" 1> /dev/null
22+
23+
$ josh-filter ":\"x\""
24+
25+
$ git log --graph --pretty=%s:%H HEAD
26+
* add file3:667a912db7482f3c8023082c9b4c7b267792633a
27+
* add file2:81b10fb4984d20142cd275b89c91c346e536876a
28+
* add file1:bb282e9cdc1b972fffd08fd21eead43bc0c83cb8
29+
30+
$ git log --graph --pretty=%s:%H FILTERED_HEAD
31+
* x:9d117d96dfdba145df43ebe37d9e526acac4b17c
32+
* x:b232aa8eefaadfb5e38b3ad7355118aa59fb651e
33+
* x:6b4d1f87c2be08f7d0f9d40b6679aab612e259b1
34+
35+
$ josh-filter -p ":from(81b10fb4984d20142cd275b89c91c346e536876a:\"x\")"
36+
:"x":concat(81b10fb4984d20142cd275b89c91c346e536876a:"x")
37+
$ josh-filter ":from(81b10fb4984d20142cd275b89c91c346e536876a:\"x\")"
38+
39+
$ git log --graph --pretty=%s FILTERED_HEAD
40+
* x
41+
* add file2
42+
* add file1

0 commit comments

Comments
 (0)