Skip to content

Commit e8feff5

Browse files
Add "pin" filter
"pin" filter holds off updates to the tree, choosing versions from parent, or preventing versions from appearing.
1 parent 9ad4af4 commit e8feff5

File tree

8 files changed

+547
-4
lines changed

8 files changed

+547
-4
lines changed

docs/src/reference/filters.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,22 @@ 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+
Refer to `pin_filter_workspace.t` and `pin_filter_hook.t` for reference.
139+
124140
Filter order matters
125141
--------------------
126142

josh-core/src/filter/mod.rs

Lines changed: 75 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ impl std::fmt::Debug for Filter {
5757
}
5858
}
5959

60+
#[derive(Debug)]
6061
pub struct Apply<'a> {
6162
tree: git2::Tree<'a>,
6263
pub author: Option<(String, String)>,
@@ -297,6 +298,7 @@ enum Op {
297298
Chain(Filter, Filter),
298299
Subtract(Filter, Filter),
299300
Exclude(Filter),
301+
Pin(Filter),
300302
}
301303

302304
/// Pretty print the filter on multiple lines with initial indentation level.
@@ -342,6 +344,10 @@ fn pretty2(op: &Op, indent: usize, compose: bool) -> String {
342344
Op::Compose(filters) => ff(&filters, "exclude", indent),
343345
b => format!(":exclude[{}]", pretty2(&b, indent, false)),
344346
},
347+
Op::Pin(filter) => match to_op(*filter) {
348+
Op::Compose(filters) => ff(&filters, "pin", indent),
349+
b => format!(":pin[{}]", pretty2(&b, indent, false)),
350+
},
345351
Op::Chain(a, b) => match (to_op(*a), to_op(*b)) {
346352
(Op::Subdir(p1), Op::Prefix(p2)) if p1 == p2 => {
347353
format!("::{}/", parse::quote_if(&p1.to_string_lossy()))
@@ -392,7 +398,7 @@ pub fn nesting(filter: Filter) -> usize {
392398
fn nesting2(op: &Op) -> usize {
393399
match op {
394400
Op::Compose(filters) => 1 + filters.iter().map(|f| nesting(*f)).fold(0, |a, b| a.max(b)),
395-
Op::Exclude(filter) => 1 + nesting(*filter),
401+
Op::Exclude(filter) | Op::Pin(filter) => 1 + nesting(*filter),
396402
Op::Workspace(_) => usize::MAX / 2, // divide by 2 to make sure there is enough headroom to avoid overflows
397403
Op::Hook(_) => usize::MAX / 2, // divide by 2 to make sure there is enough headroom to avoid overflows
398404
Op::Chain(a, b) => 1 + nesting(*a).max(nesting(*b)),
@@ -430,7 +436,7 @@ fn lazy_refs2(op: &Op) -> Vec<String> {
430436
acc
431437
})
432438
}
433-
Op::Exclude(filter) => lazy_refs(*filter),
439+
Op::Exclude(filter) | Op::Pin(filter) => lazy_refs(*filter),
434440
Op::Chain(a, b) => {
435441
let mut av = lazy_refs(*a);
436442
av.append(&mut lazy_refs(*b));
@@ -481,6 +487,7 @@ fn resolve_refs2(refs: &std::collections::HashMap<String, git2::Oid>, op: &Op) -
481487
Op::Compose(filters.iter().map(|f| resolve_refs(refs, *f)).collect())
482488
}
483489
Op::Exclude(filter) => Op::Exclude(resolve_refs(refs, *filter)),
490+
Op::Pin(filter) => Op::Pin(resolve_refs(refs, *filter)),
484491
Op::Chain(a, b) => Op::Chain(resolve_refs(refs, *a), resolve_refs(refs, *b)),
485492
Op::Subtract(a, b) => Op::Subtract(resolve_refs(refs, *a), resolve_refs(refs, *b)),
486493
Op::Rev(filters) => {
@@ -565,6 +572,9 @@ fn spec2(op: &Op) -> String {
565572
Op::Exclude(b) => {
566573
format!(":exclude[{}]", spec(*b))
567574
}
575+
Op::Pin(filter) => {
576+
format!(":pin[{}]", spec(*filter))
577+
}
568578
Op::Rev(filters) => {
569579
let mut v = filters
570580
.iter()
@@ -708,6 +718,9 @@ fn as_tree2(repo: &git2::Repository, op: &Op) -> JoshResult<git2::Oid> {
708718
Op::Exclude(b) => {
709719
builder.insert("exclude", as_tree(repo, *b)?, git2::FileMode::Tree.into())?;
710720
}
721+
Op::Pin(b) => {
722+
builder.insert("pin", as_tree(repo, *b)?, git2::FileMode::Tree.into())?;
723+
}
711724
Op::Subdir(path) => {
712725
builder.insert(
713726
"subdir",
@@ -1084,6 +1097,11 @@ fn from_tree2(repo: &git2::Repository, tree_oid: git2::Oid) -> JoshResult<Op> {
10841097
let filter = from_tree2(repo, exclude_tree.id())?;
10851098
Ok(Op::Exclude(to_filter(filter)))
10861099
}
1100+
"pin" => {
1101+
let pin_tree = repo.find_tree(entry.id())?;
1102+
let filter = from_tree2(repo, pin_tree.id())?;
1103+
Ok(Op::Pin(to_filter(filter)))
1104+
}
10871105
"rev" => {
10881106
let rev_tree = repo.find_tree(entry.id())?;
10891107
let mut filters = std::collections::BTreeMap::new();
@@ -1810,6 +1828,8 @@ fn apply2<'a>(transaction: &'a cache::Transaction, op: &Op, x: Apply<'a>) -> Jos
18101828
return apply(transaction, *b, apply(transaction, *a, x.clone())?);
18111829
}
18121830
Op::Hook(_) => Err(josh_error("not applicable to tree")),
1831+
1832+
Op::Pin(_) => Ok(x),
18131833
}
18141834
}
18151835

@@ -2055,6 +2075,23 @@ pub fn is_linear(filter: Filter) -> bool {
20552075
}
20562076
}
20572077

2078+
fn legalize_pin<F>(f: Filter, c: &F) -> Filter
2079+
where
2080+
F: Fn(Filter) -> Filter,
2081+
{
2082+
match to_op(f) {
2083+
Op::Compose(f) => {
2084+
let f = f.into_iter().map(|f| legalize_pin(f, c)).collect();
2085+
to_filter(Op::Compose(f))
2086+
}
2087+
Op::Chain(a, b) => to_filter(Op::Chain(legalize_pin(a, c), legalize_pin(b, c))),
2088+
Op::Subtract(a, b) => to_filter(Op::Subtract(legalize_pin(a, c), legalize_pin(b, c))),
2089+
Op::Exclude(f) => to_filter(Op::Exclude(legalize_pin(f, c))),
2090+
Op::Pin(f) => c(f),
2091+
_ => f,
2092+
}
2093+
}
2094+
20582095
fn per_rev_filter(
20592096
transaction: &cache::Transaction,
20602097
commit: &git2::Commit,
@@ -2085,13 +2122,48 @@ fn per_rev_filter(
20852122
.map(|parent| transaction.get(filter, parent))
20862123
.collect::<Option<Vec<git2::Oid>>>();
20872124
let normal_parents = some_or!(normal_parents, { return Ok(None) });
2125+
2126+
// Special case: `:pin` filter needs to be aware of filtered history
2127+
let pin_details = if let Some(&parent) = normal_parents.first() {
2128+
let legalized_a = legalize_pin(cw, &|f| f);
2129+
let legalized_b = legalize_pin(cw, &|f| to_filter(Op::Exclude(f)));
2130+
2131+
if legalized_a != legalized_b {
2132+
let pin_subtract = apply(
2133+
transaction,
2134+
opt::optimize(to_filter(Op::Subtract(legalized_a, legalized_b))),
2135+
Apply::from_commit(commit)?,
2136+
)?;
2137+
2138+
let parent = transaction.repo().find_commit(parent)?;
2139+
let parent = parent.tree()?;
2140+
2141+
let pin_subtract = pin_subtract.tree();
2142+
let pin_overlay = tree::transpose(transaction, pin_subtract, &parent)?;
2143+
2144+
Some((pin_subtract.id(), pin_overlay.id()))
2145+
} else {
2146+
None
2147+
}
2148+
} else {
2149+
None
2150+
};
2151+
20882152
let filtered_parent_ids: Vec<_> = normal_parents.into_iter().chain(extra_parents).collect();
20892153

2090-
let tree_data = apply(
2154+
let mut tree_data = apply(
20912155
transaction,
20922156
commit_filter,
20932157
Apply::from_commit(commit)?.with_parents(filtered_parent_ids.clone()),
20942158
)?;
2159+
2160+
if let Some((pin_subtract, pin_overlay)) = pin_details {
2161+
let with_exclude = tree::subtract(transaction, tree_data.tree().id(), pin_subtract)?;
2162+
let with_overlay = tree::overlay(transaction, pin_overlay, with_exclude)?;
2163+
2164+
tree_data = tree_data.with_tree(transaction.repo().find_tree(with_overlay)?);
2165+
}
2166+
20952167
return Some(history::create_filtered_commit(
20962168
commit,
20972169
filtered_parent_ids,

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: 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))),

josh-core/src/filter/tree.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use super::*;
2+
use std::path::PathBuf;
23

34
use crate::cache::TransactionContext;
45
use crate::cache_stack::CacheStack;
@@ -1001,6 +1002,41 @@ pub fn get_blob(repo: &git2::Repository, tree: &git2::Tree, path: &Path) -> Stri
10011002
content.to_owned()
10021003
}
10031004

1005+
/// Compose a new tree by taking paths out of `paths`,
1006+
/// and actual blobs out of `blobs`.
1007+
pub fn transpose<'a>(
1008+
transaction: &'a cache::Transaction,
1009+
paths: &'a git2::Tree,
1010+
blobs: &'a git2::Tree,
1011+
) -> JoshResult<git2::Tree<'a>> {
1012+
use git2::build::TreeUpdateBuilder;
1013+
use git2::{TreeWalkMode, TreeWalkResult};
1014+
1015+
let mut builder = TreeUpdateBuilder::new();
1016+
1017+
paths.walk(TreeWalkMode::PostOrder, |path, entry| {
1018+
let path = PathBuf::from(path).join(entry.name().unwrap_or(""));
1019+
1020+
let entry = match blobs.get_path(&path) {
1021+
Ok(entry) => entry,
1022+
Err(e) if e.code() == git2::ErrorCode::NotFound => return TreeWalkResult::Ok,
1023+
Err(_) => return TreeWalkResult::Abort,
1024+
};
1025+
1026+
if entry.kind() != Some(git2::ObjectType::Blob) {
1027+
return TreeWalkResult::Ok;
1028+
}
1029+
1030+
builder.upsert(path, entry.id(), git2::FileMode::Blob);
1031+
TreeWalkResult::Ok
1032+
})?;
1033+
1034+
let repo = transaction.repo();
1035+
let tree = builder.create_updated(repo, &empty(repo))?;
1036+
1037+
Ok(repo.find_tree(tree)?)
1038+
}
1039+
10041040
pub fn empty_id() -> git2::Oid {
10051041
git2::Oid::from_str("4b825dc642cb6eb9a060e54bf8d69288fbee4904").unwrap()
10061042
}

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)