Skip to content

Commit c084f68

Browse files
Implement stored filter
1 parent 057bca3 commit c084f68

File tree

13 files changed

+816
-4
lines changed

13 files changed

+816
-4
lines changed

docs/src/reference/filters.md

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,22 @@ Remove all paths present in the *output* of ``:filter`` from the input tree.
6666
It should generally be avoided to use any filters that change paths and instead only
6767
use filters that select paths without altering them.
6868

69+
### Stored **`:+path/to/file`**
70+
Looks for a file with a ``.josh`` extension at the specified path and applies the filter defined in that file.
71+
The path argument should be provided *without* the ``.josh`` extension, as it will be automatically appended.
72+
73+
For example, ``:+st/config`` will look for a file at ``st/config.josh`` and apply the filter defined in that file.
74+
The resulting tree will contain the contents specified by the filter in the ``.josh`` file.
75+
76+
Stored filters apply from the root of the repository, making them useful for configuration files that define
77+
filters to be applied at the repository root level.
78+
6979
### Workspace **`:workspace=a`**
70-
Similar to ``:/a`` but also looks for a ``workspace.josh`` file inside the
71-
specified directory (called the "workspace root").
72-
The resulting tree will contain the contents of the
80+
Like stored filters, but with an additional "base" component that first selects the specified directory
81+
(called the "workspace root") before applying the filter from the ``workspace.josh`` file inside it.
82+
83+
The workspace filter is equivalent to ``:/a`` combined with applying a stored filter, where the filter is read
84+
from ``workspace.josh`` within the selected directory. The resulting tree will contain the contents of the
7385
workspace root as well as additional files specified in the ``workspace.josh`` file.
7486
(see [Workspaces](./workspace.md))
7587

josh-core/src/filter/grammar.pest

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,15 @@ filter_spec = { (
2929
| filter_squash
3030
| filter_presub
3131
| filter_subdir
32+
| filter_stored
3233
| filter_nop
3334
| filter
3435
| filter_noarg
3536
)+ }
3637

3738
filter_group = { CMD_START ~ cmd? ~ GROUP_START ~ compose ~ GROUP_END }
3839
filter_subdir = { CMD_START ~ "/" ~ argument }
40+
filter_stored = { CMD_START ~ "+" ~ argument }
3941
filter_nop = { CMD_START ~ "/" }
4042
filter_presub = { CMD_START ~ ":" ~ argument ~ ("=" ~ argument)? }
4143
filter = { CMD_START ~ cmd ~ "=" ~ (argument ~ (";" ~ argument)*)? }

josh-core/src/filter/mod.rs

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,7 @@ enum Op {
312312
Prefix(std::path::PathBuf),
313313
Subdir(std::path::PathBuf),
314314
Workspace(std::path::PathBuf),
315+
Stored(std::path::PathBuf),
315316

316317
Pattern(String),
317318
Message(String, regex::Regex),
@@ -589,6 +590,9 @@ fn spec2(op: &Op) -> String {
589590
Op::Workspace(path) => {
590591
format!(":workspace={}", parse::quote_if(&path.to_string_lossy()))
591592
}
593+
Op::Stored(path) => {
594+
format!(":+{}", parse::quote_if(&path.to_string_lossy()))
595+
}
592596
Op::RegexReplace(replacements) => {
593597
let v = replacements
594598
.iter()
@@ -737,6 +741,31 @@ fn get_workspace<'a>(repo: &'a git2::Repository, tree: &'a git2::Tree<'a>, path:
737741
)
738742
}
739743

744+
// Handle stored.josh files that contain ":stored=..." as their only filter as
745+
// a "redirect" to that other stored. We chain an exclude of the redirecting stored
746+
// in front to prevent infinite recursion.
747+
fn resolve_stored_redirect<'a>(
748+
repo: &'a git2::Repository,
749+
tree: &'a git2::Tree<'a>,
750+
path: &Path,
751+
) -> Option<(Filter, std::path::PathBuf)> {
752+
let stored_path = path.with_extension("josh");
753+
let f = parse::parse(&tree::get_blob(repo, tree, &stored_path))
754+
.unwrap_or_else(|_| to_filter(Op::Empty));
755+
756+
if let Op::Stored(p) = to_op(f) {
757+
Some((chain(to_filter(Op::Exclude(file(stored_path))), f), p))
758+
} else {
759+
None
760+
}
761+
}
762+
763+
fn get_stored<'a>(repo: &'a git2::Repository, tree: &'a git2::Tree<'a>, path: &Path) -> Filter {
764+
let stored_path = path.with_extension("josh");
765+
let sj_file = file(stored_path.clone());
766+
compose(sj_file, get_filter(repo, tree, &stored_path))
767+
}
768+
740769
fn get_filter<'a>(repo: &'a git2::Repository, tree: &'a git2::Tree<'a>, path: &Path) -> Filter {
741770
let ws_path = normalize_path(path);
742771
let ws_id = ok_or!(tree.get_path(&ws_path), {
@@ -1022,6 +1051,33 @@ fn apply_to_commit2(
10221051

10231052
return per_rev_filter(transaction, commit, filter, commit_filter, parent_filters);
10241053
}
1054+
Op::Stored(s_path) => {
1055+
if let Some((redirect, _)) = resolve_stored_redirect(repo, &commit.tree()?, s_path) {
1056+
if let Some(r) = apply_to_commit2(&to_op(redirect), commit, transaction)? {
1057+
transaction.insert(filter, commit.id(), r, true);
1058+
return Ok(Some(r));
1059+
} else {
1060+
return Ok(None);
1061+
}
1062+
}
1063+
1064+
let commit_filter = get_stored(repo, &commit.tree()?, &s_path);
1065+
1066+
let parent_filters = commit
1067+
.parents()
1068+
.map(|parent| {
1069+
rs_tracing::trace_scoped!("parent", "id": parent.id().to_string());
1070+
let pcs = get_stored(
1071+
repo,
1072+
&parent.tree().unwrap_or_else(|_| tree::empty(repo)),
1073+
&s_path,
1074+
);
1075+
Ok((parent, pcs))
1076+
})
1077+
.collect::<JoshResult<Vec<_>>>()?;
1078+
1079+
return per_rev_filter(transaction, commit, filter, commit_filter, parent_filters);
1080+
}
10251081
Op::Fold => {
10261082
let filtered_parent_ids = commit
10271083
.parents()
@@ -1207,6 +1263,7 @@ fn apply2<'a>(transaction: &'a cache::Transaction, op: &Op, x: Apply<'a>) -> Jos
12071263
}
12081264

12091265
Op::Workspace(path) => apply(transaction, get_workspace(repo, &x.tree(), &path), x),
1266+
Op::Stored(path) => apply(transaction, get_stored(repo, &x.tree(), &path), x),
12101267

12111268
Op::Compose(filters) => {
12121269
let filtered: Vec<_> = filters
@@ -1329,6 +1386,31 @@ fn unapply_workspace<'a>(
13291386

13301387
Ok(Some(result))
13311388
}
1389+
Op::Stored(path) => {
1390+
let stored_path = path.with_extension("josh");
1391+
let stored = get_filter(transaction.repo(), &tree, &stored_path);
1392+
let original_stored = get_filter(transaction.repo(), &parent_tree, &stored_path);
1393+
1394+
let sj_file = file(stored_path.clone());
1395+
let filter = compose(sj_file, stored);
1396+
let original_filter = compose(sj_file, original_stored);
1397+
let filtered = apply(
1398+
transaction,
1399+
original_filter,
1400+
Apply::from_tree(parent_tree.clone()),
1401+
)?;
1402+
let matching = apply(transaction, invert(original_filter)?, filtered.clone())?;
1403+
let stripped = tree::subtract(transaction, parent_tree.id(), matching.tree().id())?;
1404+
let x = apply(transaction, invert(filter)?, Apply::from_tree(tree))?;
1405+
1406+
let result = transaction.repo().find_tree(tree::overlay(
1407+
transaction,
1408+
x.tree().id(),
1409+
stripped,
1410+
)?)?;
1411+
1412+
Ok(Some(result))
1413+
}
13321414
_ => Ok(None),
13331415
}
13341416
}
@@ -1397,6 +1479,17 @@ pub fn compute_warnings<'a>(
13971479
}
13981480
}
13991481

1482+
if let Op::Stored(path) = to_op(filter) {
1483+
let stored_path = path.with_extension("josh");
1484+
let stored_filter = &tree::get_blob(transaction.repo(), &tree, &stored_path);
1485+
if let Ok(res) = parse(stored_filter) {
1486+
filter = res;
1487+
} else {
1488+
warnings.push("couldn't parse stored\n".to_string());
1489+
return warnings;
1490+
}
1491+
}
1492+
14001493
let filter = opt::flatten(filter);
14011494
if let Op::Compose(filters) = to_op(filter) {
14021495
for f in filters {

josh-core/src/filter/parse.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,9 @@ fn parse_item(pair: pest::iterators::Pair<Rule>) -> JoshResult<Op> {
8484
Rule::filter_subdir => Ok(Op::Subdir(
8585
Path::new(&unquote(pair.into_inner().next().unwrap().as_str())).to_owned(),
8686
)),
87+
Rule::filter_stored => Ok(Op::Stored(
88+
Path::new(&unquote(pair.into_inner().next().unwrap().as_str())).to_owned(),
89+
)),
8790
Rule::filter_presub => {
8891
let mut inner = pair.into_inner();
8992
let arg = &unquote(inner.next().unwrap().as_str());

josh-core/src/filter/persist.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,10 @@ impl InMemoryBuilder {
228228
let blob = self.write_blob(path.to_string_lossy().as_bytes());
229229
push_blob_entries(&mut entries, [("workspace", blob)]);
230230
}
231+
Op::Stored(path) => {
232+
let blob = self.write_blob(path.to_string_lossy().as_bytes());
233+
push_blob_entries(&mut entries, [("stored", blob)]);
234+
}
231235
Op::Nop => {
232236
let blob = self.write_blob(b"");
233237
push_blob_entries(&mut entries, [("nop", blob)]);
@@ -507,6 +511,11 @@ fn from_tree2(repo: &git2::Repository, tree_oid: git2::Oid) -> JoshResult<Op> {
507511
let path = std::str::from_utf8(blob.content())?;
508512
Ok(Op::Workspace(std::path::PathBuf::from(path)))
509513
}
514+
"stored" => {
515+
let blob = repo.find_blob(entry.id())?;
516+
let path = std::str::from_utf8(blob.content())?;
517+
Ok(Op::Stored(std::path::PathBuf::from(path)))
518+
}
510519
"compose" => {
511520
let compose_tree = repo.find_tree(entry.id())?;
512521
let mut filters = Vec::new();
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
$ export TERM=dumb
2+
$ export RUST_LOG_STYLE=never
3+
4+
$ git init -q real_repo 1> /dev/null
5+
$ cd real_repo
6+
7+
$ mkdir sub1
8+
$ echo contents1 > sub1/file1
9+
$ git add sub1
10+
$ git commit -m "add file1" 1> /dev/null
11+
12+
$ mkdir -p sub2/subsub
13+
$ echo contents1 > sub2/subsub/file2
14+
$ git add sub2
15+
$ git commit -m "add file2" 1> /dev/null
16+
17+
$ mkdir -p sub3
18+
$ echo contents1 > sub3/sub_file
19+
$ git add .
20+
$ git commit -m "add sub_file" 1> /dev/null
21+
22+
$ mkdir st
23+
$ cat > st/config.josh <<EOF
24+
> x = :[::sub2/subsub/,::sub1/]
25+
> EOF
26+
$ mkdir st2
27+
$ cat > st2/config.josh <<EOF
28+
> :[
29+
> a = :[::sub2/subsub/,::sub3/]
30+
> :/sub1:prefix=blub
31+
> ]:prefix=xyz
32+
> EOF
33+
$ git add .
34+
$ git commit -m "add st" 1> /dev/null
35+
36+
$ tree
37+
.
38+
|-- st
39+
| `-- config.josh
40+
|-- st2
41+
| `-- config.josh
42+
|-- sub1
43+
| `-- file1
44+
|-- sub2
45+
| `-- subsub
46+
| `-- file2
47+
`-- sub3
48+
`-- sub_file
49+
50+
7 directories, 5 files
51+
52+
$ josh-filter -s :+st/config
53+
[2] :+st/config
54+
[2] :[
55+
::sub1/
56+
::sub2/subsub/
57+
]
58+
[2] :prefix=x
59+
[4] sequence_number
60+
61+
$ git log --graph --pretty=%s FILTERED_HEAD
62+
* add st
63+
* add file2
64+
* add file1
65+
66+
$ git checkout FILTERED_HEAD 2> /dev/null
67+
$ tree
68+
.
69+
|-- st
70+
| `-- config.josh
71+
`-- x
72+
|-- sub1
73+
| `-- file1
74+
`-- sub2
75+
`-- subsub
76+
`-- file2
77+
78+
6 directories, 3 files
79+
80+
$ git checkout master 2> /dev/null
81+
$ josh-filter -s :+st2/config
82+
[2] :+st/config
83+
[2] :+st2/config
84+
[2] :[
85+
::sub1/
86+
::sub2/subsub/
87+
]
88+
[2] :prefix=x
89+
[3] :[
90+
a = :[
91+
::sub2/subsub/
92+
::sub3/
93+
]
94+
blub = :/sub1
95+
]
96+
[3] :prefix=xyz
97+
[7] sequence_number
98+
99+
$ git log --graph --pretty=%s FILTERED_HEAD
100+
* add st
101+
* add sub_file
102+
* add file2
103+
* add file1
104+
105+
$ git checkout FILTERED_HEAD 2> /dev/null
106+
$ tree
107+
.
108+
|-- st2
109+
| `-- config.josh
110+
`-- xyz
111+
|-- a
112+
| |-- sub2
113+
| | `-- subsub
114+
| | `-- file2
115+
| `-- sub3
116+
| `-- sub_file
117+
`-- blub
118+
`-- file1
119+
120+
8 directories, 4 files
121+
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
$ export TERM=dumb
2+
$ export RUST_LOG_STYLE=never
3+
4+
$ git init -q real_repo 1> /dev/null
5+
$ cd real_repo
6+
7+
$ mkdir sub1
8+
$ echo contents1 > sub1/file1
9+
$ git add sub1
10+
$ git commit -m "add file1" 1> /dev/null
11+
12+
$ mkdir -p sub2/subsub
13+
$ echo contents1 > sub2/subsub/file2
14+
$ git add sub2
15+
$ git commit -m "add file2" 1> /dev/null
16+
17+
$ mkdir st
18+
$ cat > st/config.josh <<EOF
19+
> ::sub2/subsub/
20+
> ::sub1/
21+
> EOF
22+
$ git add st
23+
$ git commit -m "add st" 1> /dev/null
24+
25+
$ josh-filter -s :+st/config master --update refs/josh/master
26+
[2] :+st/config
27+
[2] :[
28+
::sub1/
29+
::sub2/subsub/
30+
]
31+
[3] sequence_number
32+
33+
$ git log --graph --pretty=%s refs/josh/master
34+
* add st
35+
* add file2
36+
* add file1
37+
38+
$ git checkout refs/josh/master 2> /dev/null
39+
$ tree
40+
.
41+
|-- st
42+
| `-- config.josh
43+
|-- sub1
44+
| `-- file1
45+
`-- sub2
46+
`-- subsub
47+
`-- file2
48+
49+
5 directories, 3 files
50+

0 commit comments

Comments
 (0)