Skip to content

Commit 300d1e9

Browse files
Add "pin" filter
"pin" filter holds off updates to the tree, choosing versions from parent, or preventing versions from appearing.
1 parent af4fd85 commit 300d1e9

File tree

7 files changed

+479
-3
lines changed

7 files changed

+479
-3
lines changed

docs/src/reference/filters.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,27 @@ tree.
121121
Normally Josh will keep all commits in the filtered history whose tree differs from any of it's
122122
parents.
123123

124+
### Pin tree contents
125+
126+
Pin revision of a subtree to revision of the parent commit.
127+
128+
In practical terms, it means that file and folder updates are "held off", and revisions are "pinned".
129+
If a tree entry already existed in the parent revision, that version will be chosen.
130+
Otherwise, the tree entry will not appear in the filtered commit.
131+
132+
The source of the parent revision is always the first commit parent.
133+
134+
Note that this filter is only practical when used with `:hook` or `workspace.josh`,
135+
as it should apply per-revision only. Applying `:pin` for the whole history
136+
will result in the subtree being excluded from all revisions.
137+
138+
#### Limitations
139+
140+
At the moment, this filter only works as a top level filter. This constraint
141+
also applies within workspaces and hook filters.
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/mod.rs

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,7 @@ enum Op {
297297
Chain(Filter, Filter),
298298
Subtract(Filter, Filter),
299299
Exclude(Filter),
300+
Pin(Filter),
300301
}
301302

302303
/// Pretty print the filter on multiple lines with initial indentation level.
@@ -342,6 +343,10 @@ fn pretty2(op: &Op, indent: usize, compose: bool) -> String {
342343
Op::Compose(filters) => ff(&filters, "exclude", indent),
343344
b => format!(":exclude[{}]", pretty2(&b, indent, false)),
344345
},
346+
Op::Pin(filter) => match to_op(*filter) {
347+
Op::Compose(filters) => ff(&filters, "pin", indent),
348+
b => format!(":pin[{}]", pretty2(&b, indent, false)),
349+
},
345350
Op::Chain(a, b) => match (to_op(*a), to_op(*b)) {
346351
(Op::Subdir(p1), Op::Prefix(p2)) if p1 == p2 => {
347352
format!("::{}/", parse::quote_if(&p1.to_string_lossy()))
@@ -392,7 +397,7 @@ pub fn nesting(filter: Filter) -> usize {
392397
fn nesting2(op: &Op) -> usize {
393398
match op {
394399
Op::Compose(filters) => 1 + filters.iter().map(|f| nesting(*f)).fold(0, |a, b| a.max(b)),
395-
Op::Exclude(filter) => 1 + nesting(*filter),
400+
Op::Exclude(filter) | Op::Pin(filter) => 1 + nesting(*filter),
396401
Op::Workspace(_) => usize::MAX / 2, // divide by 2 to make sure there is enough headroom to avoid overflows
397402
Op::Hook(_) => usize::MAX / 2, // divide by 2 to make sure there is enough headroom to avoid overflows
398403
Op::Chain(a, b) => 1 + nesting(*a).max(nesting(*b)),
@@ -430,7 +435,7 @@ fn lazy_refs2(op: &Op) -> Vec<String> {
430435
acc
431436
})
432437
}
433-
Op::Exclude(filter) => lazy_refs(*filter),
438+
Op::Exclude(filter) | Op::Pin(filter) => lazy_refs(*filter),
434439
Op::Chain(a, b) => {
435440
let mut av = lazy_refs(*a);
436441
av.append(&mut lazy_refs(*b));
@@ -481,6 +486,7 @@ fn resolve_refs2(refs: &std::collections::HashMap<String, git2::Oid>, op: &Op) -
481486
Op::Compose(filters.iter().map(|f| resolve_refs(refs, *f)).collect())
482487
}
483488
Op::Exclude(filter) => Op::Exclude(resolve_refs(refs, *filter)),
489+
Op::Pin(filter) => Op::Pin(resolve_refs(refs, *filter)),
484490
Op::Chain(a, b) => Op::Chain(resolve_refs(refs, *a), resolve_refs(refs, *b)),
485491
Op::Subtract(a, b) => Op::Subtract(resolve_refs(refs, *a), resolve_refs(refs, *b)),
486492
Op::Rev(filters) => {
@@ -565,6 +571,9 @@ fn spec2(op: &Op) -> String {
565571
Op::Exclude(b) => {
566572
format!(":exclude[{}]", spec(*b))
567573
}
574+
Op::Pin(filter) => {
575+
format!(":pin[{}]", spec(*filter))
576+
}
568577
Op::Rev(filters) => {
569578
let mut v = filters
570579
.iter()
@@ -708,6 +717,9 @@ fn as_tree2(repo: &git2::Repository, op: &Op) -> JoshResult<git2::Oid> {
708717
Op::Exclude(b) => {
709718
builder.insert("exclude", as_tree(repo, *b)?, git2::FileMode::Tree.into())?;
710719
}
720+
Op::Pin(b) => {
721+
builder.insert("pin", as_tree(repo, *b)?, git2::FileMode::Tree.into())?;
722+
}
711723
Op::Subdir(path) => {
712724
builder.insert(
713725
"subdir",
@@ -1084,6 +1096,11 @@ fn from_tree2(repo: &git2::Repository, tree_oid: git2::Oid) -> JoshResult<Op> {
10841096
let filter = from_tree2(repo, exclude_tree.id())?;
10851097
Ok(Op::Exclude(to_filter(filter)))
10861098
}
1099+
"pin" => {
1100+
let pin_tree = repo.find_tree(entry.id())?;
1101+
let filter = from_tree2(repo, pin_tree.id())?;
1102+
Ok(Op::Pin(to_filter(filter)))
1103+
}
10871104
"rev" => {
10881105
let rev_tree = repo.find_tree(entry.id())?;
10891106
let mut filters = std::collections::BTreeMap::new();
@@ -1810,6 +1827,25 @@ fn apply2<'a>(transaction: &'a cache::Transaction, op: &Op, x: Apply<'a>) -> Jos
18101827
return apply(transaction, *b, apply(transaction, *a, x.clone())?);
18111828
}
18121829
Op::Hook(_) => Err(josh_error("not applicable to tree")),
1830+
1831+
Op::Pin(pin_filter) => {
1832+
let filtered_parent = if let Some(parent) = x.parents.as_ref().and_then(|p| p.first()) {
1833+
let parent = repo.find_commit(*parent)?;
1834+
let filtered = apply(transaction, *pin_filter, Apply::from_commit(&parent)?)?;
1835+
filtered.tree.id()
1836+
} else {
1837+
tree::empty_id()
1838+
};
1839+
1840+
// Mask out all the "pinned" files from current tree
1841+
let exclude = to_filter(Op::Exclude(*pin_filter));
1842+
let with_mask = apply(transaction, exclude, x.clone())?;
1843+
1844+
// Overlay filtered parent tree on current one to override versions
1845+
let with_overlay = tree::overlay(transaction, with_mask.tree.id(), filtered_parent)?;
1846+
1847+
Ok(x.with_tree(repo.find_tree(with_overlay)?))
1848+
}
18131849
}
18141850
}
18151851

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,
@@ -502,6 +505,7 @@ pub fn invert(filter: Filter) -> JoshResult<Filter> {
502505
Op::Pattern(pattern) => Some(Op::Pattern(pattern)),
503506
Op::Rev(_) => Some(Op::Nop),
504507
Op::RegexReplace(_) => Some(Op::Nop),
508+
Op::Pin(_) => Some(Op::Nop),
505509
_ => None,
506510
};
507511

josh-core/src/filter/parse.rs

Lines changed: 1 addition & 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))),

tests/filter/pin_compose.t

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
$ export GIT_TREE_FMT='%(objectmode) %(objecttype) %(objectname) %(path)'
2+
3+
$ export TESTTMP=${PWD}
4+
$ cd ${TESTTMP}
5+
6+
$ git init -q repo
7+
$ cd repo
8+
$ mkdir -p josh/overlay
9+
$ mkdir -p code
10+
11+
Populate repo contents for the first commit
12+
13+
$ cat << EOF > code/app.js
14+
> async fn main() {
15+
> await fetch("http://127.0.0.1");
16+
> }
17+
> EOF
18+
19+
$ cat << EOF > code/lib.js
20+
> fn log() {
21+
> console.log("logged!");
22+
> }
23+
> EOF
24+
25+
Also create a workspace with the tree overlay filter
26+
27+
We first select files in josh/overlay, whatever is in there
28+
will take priority over the next tree in the composition filter
29+
30+
$ mkdir -p workspaces/overlay
31+
$ cat << EOF > workspaces/overlay/workspace.josh
32+
> :[
33+
> :/code
34+
> :/josh/overlay
35+
> ]
36+
> EOF
37+
38+
Here's the repo layout at this point:
39+
40+
$ tree .
41+
.
42+
|-- code
43+
| |-- app.js
44+
| `-- lib.js
45+
|-- josh
46+
| `-- overlay
47+
`-- workspaces
48+
`-- overlay
49+
`-- workspace.josh
50+
51+
6 directories, 3 files
52+
53+
Commit this:
54+
55+
$ git add .
56+
$ git commit -q -m "first commit"
57+
58+
Now, filter the ws and check the result
59+
60+
$ josh-filter ':workspace=workspaces/overlay'
61+
$ git ls-tree --format="${GIT_TREE_FMT}" -r FILTERED_HEAD
62+
100644 blob 0747fcb9cd688a7876932dcc30006e6ffa9106d6 app.js
63+
100644 blob 5910ad90fda519a6cc9299d4688679d56dc8d6dd lib.js
64+
100644 blob 39dc0f50ad353a5ee880b4a87ecc06dee7b48c92 workspace.josh
65+
66+
Save the OID of app.js before making changes:
67+
68+
$ export ORIGINAL_APP_OID=$(git ls-tree --format="%(objectname)" FILTERED_HEAD app.js)
69+
$ echo "${ORIGINAL_APP_OID}"
70+
0747fcb9cd688a7876932dcc30006e6ffa9106d6
71+
72+
Make next commit: both files will change
73+
74+
$ cat << EOF > code/app.js
75+
> async fn main() {
76+
> await fetch("http://internal-secret-portal.company.com");
77+
> }
78+
> EOF
79+
80+
$ cat << EOF > code/lib.js
81+
> fn log() {
82+
> console.log("INFO: logged!");
83+
> }
84+
> EOF
85+
86+
$ git add code/app.js code/lib.js
87+
88+
Insert the old app.js OID into the overlay.
89+
Note that we aren't copying the file -- we are directly referencing the OID.
90+
This ensures it's the same entry in git ODB.
91+
92+
$ git update-index --add --cacheinfo 100644,"${ORIGINAL_APP_OID}","josh/overlay/app.js"
93+
$ git commit -q -m "second commit"
94+
95+
Verify commit tree looks right:
96+
97+
$ git ls-tree -r --format="${GIT_TREE_FMT}" HEAD
98+
100644 blob 1540d15e1bdc499e31ea05703a0daaf520774a85 code/app.js
99+
100644 blob 627cdb2ef7a3eb1a2b4537ce17fea1d93bfecdd2 code/lib.js
100+
100644 blob 0747fcb9cd688a7876932dcc30006e6ffa9106d6 josh/overlay/app.js
101+
100644 blob 39dc0f50ad353a5ee880b4a87ecc06dee7b48c92 workspaces/overlay/workspace.josh
102+
103+
Filter the workspace and check the result:
104+
105+
$ josh-filter ':workspace=workspaces/overlay'
106+
107+
We can see now that the app.js file was held at the previous version:
108+
109+
$ git ls-tree --format="${GIT_TREE_FMT}" -r FILTERED_HEAD
110+
100644 blob 0747fcb9cd688a7876932dcc30006e6ffa9106d6 app.js
111+
100644 blob 627cdb2ef7a3eb1a2b4537ce17fea1d93bfecdd2 lib.js
112+
100644 blob 39dc0f50ad353a5ee880b4a87ecc06dee7b48c92 workspace.josh
113+
114+
$ git show FILTERED_HEAD:app.js
115+
async fn main() {
116+
await fetch("http://127.0.0.1");
117+
}

0 commit comments

Comments
 (0)