Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions docs/src/reference/filters.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,42 @@ Remove all paths present in the *output* of ``:filter`` from the input tree.
It should generally be avoided to use any filters that change paths and instead only
use filters that select paths without altering them.

### Invert **`:invert[:filter]`**
A shorthand syntax that applies the inverse of the composed filter. The inverse of a filter is
a filter that undoes the transformation. For example, the inverse of ``:/sub1`` (subdirectory)
is ``:prefix=sub1`` (prefix), and vice versa.

**Example:**
```
:invert[:/sub1]
```
This is equivalent to ``:prefix=sub1``, which takes the input tree and places it into
the ``sub1`` subdirectory.

Multiple filters can be provided in the compose:
```
:invert[:/sub1,:/sub2]
```
This inverts the composition of ``:/sub1`` and ``:/sub2``.

### Scope **`:<X>[..]`**
A shorthand syntax that expands to ``:X:[..]:invert[:X]``, where:
- ``:X`` is a filter (without built-in compose)
- ``:[..]`` is a compose filter (like in ``:exclude``)

This filter first applies ``:X`` to scope the input, then applies the compose filter ``:[..]``,
and finally inverts ``:X`` to restore the original scope. This is useful when you want to
apply a composition filter within a specific scope and then restore the original structure.

**Example:**
```
:<:/sub1>[::file1,::file2]
```
This is equivalent to ``:/sub1:[::file1,::file2]:invert[:/sub1]``, which:
1. Selects the ``sub1`` subdirectory (applies ``:/sub1``)
2. Applies the composition filter to select ``file1`` and ``file2`` (applies ``:[::file1,::file2]``)
3. Restores the original scope by inverting the subdirectory selection (applies ``:invert[:/sub1]``)

### Stored **`:+path/to/file`**
Looks for a file with a ``.josh`` extension at the specified path and applies the filter defined in that file.
The path argument should be provided *without* the ``.josh`` extension, as it will be automatically appended.
Expand Down
9 changes: 9 additions & 0 deletions josh-core/src/filter/grammar.pest
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ filter_spec = { (
| filter_subdir
| filter_stored
| filter_nop
| filter_scope
| filter
| filter_noarg
)+ }
Expand Down Expand Up @@ -100,6 +101,14 @@ filter_squash = {
~ ")"
}

filter_scope = {
CMD_START ~ "<"
~ NEWLINE*
~ filter_spec
~ NEWLINE*
~ ">" ~ GROUP_START ~ compose ~ GROUP_END
}

cmd = { ALNUM+ }

file_entry = { dst_path ~ "=" ~ filter_spec }
Expand Down
48 changes: 48 additions & 0 deletions josh-core/src/filter/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1770,4 +1770,52 @@ mod tests {
dst_path(parse(":[a=:/x::y/,a/b=:/i]:prefix=c").unwrap())
);
}

#[test]
fn invert_filter_parsing_test() {
// Test that :invert[X] syntax parses correctly
let filter = parse(":invert[:/sub1]").unwrap();
// Verify it's not empty
assert_ne!(filter, empty());

// Test with prefix filter (inverse of subdir)
let filter2 = parse(":invert[:prefix=sub1]").unwrap();
assert_ne!(filter2, empty());

// Test that it produces the correct inverse
let filter3 = parse(":invert[:/sub1]").unwrap();
let spec_str = spec(filter3);
// Should produce prefix (inverse of subdir)
assert!(spec_str.contains("prefix") || !spec_str.is_empty());

// Test with multiple filters in compose
let filter4 = parse(":invert[:/sub1,:/sub2]").unwrap();
assert_ne!(filter4, empty());
}

#[test]
fn scope_filter_parsing_test() {
// Test that :<X>[Y] syntax parses correctly
let filter = parse(":<:/sub1>[:/file1]").unwrap();
// Just verify parsing succeeds (filter may optimize to empty in some cases)
let _ = filter;

// Test with multiple filters in compose
let filter2 = parse(":<:/sub1>[:/file1,:/file2]").unwrap();
let _ = filter2;

// Test with prefix filter
let filter3 = parse(":<:prefix=sub1>[:prefix=file1]").unwrap();
let _ = filter3;

// Test with exclude
let filter4 = parse(":<:/sub1>[:exclude[::file1]]").unwrap();
let _ = filter4;

// Test that it expands to chain structure by checking spec output
let filter5 = parse(":<:/sub1>[:/file1]").unwrap();
let spec_str = spec(filter5);
// The spec should contain the chain representation
assert!(!spec_str.is_empty());
}
}
19 changes: 19 additions & 0 deletions josh-core/src/filter/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,10 @@ fn parse_item(pair: pest::iterators::Pair<Rule>) -> JoshResult<Op> {
match *cmd {
"pin" => Ok(Op::Pin(to_filter(Op::Compose(g)))),
"exclude" => Ok(Op::Exclude(to_filter(Op::Compose(g)))),
"invert" => {
let filter = to_filter(Op::Compose(g));
Ok(to_op(invert(filter)?))
}
"subtract" if g.len() == 2 => Ok(Op::Subtract(g[0], g[1])),
_ => Err(josh_error(&format!("parse_item: no match {:?}", cmd))),
}
Expand Down Expand Up @@ -222,7 +226,22 @@ fn parse_item(pair: pest::iterators::Pair<Rule>) -> JoshResult<Op> {

Ok(Op::Squash(Some(ids)))
}
Rule::filter_scope => {
let mut inner = pair.into_inner();
let x_filter_spec = inner
.next()
.ok_or_else(|| josh_error("filter_scope: missing filter_spec"))?;
let y_compose = inner
.next()
.ok_or_else(|| josh_error("filter_scope: missing compose"))?;

let x = parse(x_filter_spec.as_str())?;
let y_filters = parse_group(y_compose.as_str())?;
let y = to_filter(Op::Compose(y_filters));

let inverted_x = invert(x)?;
Ok(Op::Chain(x, to_filter(Op::Chain(y, inverted_x))))
}
_ => Err(josh_error("parse_item: no match")),
}
}
Expand Down
134 changes: 134 additions & 0 deletions tests/filter/scope_filter.t
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
$ export TESTTMP=${PWD}

$ cd ${TESTTMP}
$ git init -q real_repo 1> /dev/null
$ cd real_repo

$ mkdir sub1
$ echo contents1 > sub1/file1
$ mkdir sub2
$ echo contents2 > sub2/file2
$ mkdir sub3
$ echo contents3 > sub3/file3
$ git add sub1 sub2 sub3
$ git commit -m "add files" 1> /dev/null

Test basic scope filter syntax :<X>[Y]
$ FILTER_HASH=$(josh-filter -i ':<:/sub1>[:/file1]')
$ josh-filter -p ${FILTER_HASH}
sub1 = :/sub1/file1
$ git read-tree --reset -u ${FILTER_HASH}
$ tree
.
`-- chain
|-- 0
| `-- subdir
`-- 1
`-- chain
|-- 0
| `-- subdir
`-- 1
`-- prefix

7 directories, 3 files
$ cat sub1/file1
cat: sub1/file1: No such file or directory
[1]

Test scope filter with multiple filters in compose
$ FILTER_HASH=$(josh-filter -i ':<:/sub1>[:/file1,:/sub2/file2]')
$ josh-filter -p ${FILTER_HASH}
sub1 = :/sub1:[
:/file1
:/sub2/file2
]
$ git read-tree --reset -u ${FILTER_HASH}
$ tree
.
`-- chain
|-- 0
| `-- subdir
`-- 1
`-- chain
|-- 0
| `-- compose
| |-- 0
| | `-- subdir
| `-- 1
| `-- chain
| |-- 0
| | `-- subdir
| `-- 1
| `-- subdir
`-- 1
`-- prefix

13 directories, 5 files

Test scope filter with prefix filter
$ FILTER_HASH=$(josh-filter -i ':<:prefix=sub1>[:prefix=file1]')
$ josh-filter -p ${FILTER_HASH}
:empty
$ git read-tree --reset -u ${FILTER_HASH}
$ tree
.
`-- empty

1 directory, 1 file

Test scope filter with subdir and exclude
$ FILTER_HASH=$(josh-filter -i ':<:/sub1>[:exclude[::file1]]')
$ josh-filter -p ${FILTER_HASH}
sub1 = :/sub1:exclude[::file1]
$ git read-tree --reset -u ${FILTER_HASH}
$ tree
.
`-- chain
|-- 0
| `-- subdir
`-- 1
`-- chain
|-- 0
| `-- exclude
| `-- file
`-- 1
`-- prefix

8 directories, 3 files

Test scope filter verifies it expands to chain(X, chain(Y, invert(X)))
$ FILTER_HASH=$(josh-filter -i ':<:/sub1>[:/file1]')
$ josh-filter --print-filter ${FILTER_HASH}
error: unexpected argument found
[2]

Test scope filter with nested filters
$ FILTER_HASH=$(josh-filter -i ':<:/sub1>[:[:/file1,:/sub2/file2]]')
$ josh-filter -p ${FILTER_HASH}
sub1 = :/sub1:[
:/file1
:/sub2/file2
]
$ git read-tree --reset -u ${FILTER_HASH}
$ tree
.
`-- chain
|-- 0
| `-- subdir
`-- 1
`-- chain
|-- 0
| `-- compose
| |-- 0
| | `-- subdir
| `-- 1
| `-- chain
| |-- 0
| | `-- subdir
| `-- 1
| `-- subdir
`-- 1
`-- prefix

13 directories, 5 files

2 changes: 1 addition & 1 deletion tests/proxy/workspace_errors.t
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ Error in filter
remote: 1 | a/b = :b/sub2
remote: | ^---
remote: |
remote: = expected EOI, filter_group, filter_subdir, filter_stored, filter_nop, filter_presub, filter, filter_noarg, filter_message, filter_rev, filter_from, filter_concat, filter_join, filter_replace, or filter_squash
remote: = expected EOI, filter_group, filter_subdir, filter_stored, filter_nop, filter_presub, filter, filter_noarg, filter_message, filter_rev, filter_from, filter_concat, filter_join, filter_replace, filter_squash, or filter_scope
remote:
remote: a/b = :b/sub2
remote: c = :/sub1
Expand Down