Current behavior 😯
gix_attributes::Search expands a macro's sub-assignments whenever a pattern referencing the macro matches, regardless of the assignment's state. With
[attr]mylfs filter=lfs
*.bin mylfs
special.bin -mylfs
unspec.bin !mylfs
valued.bin mylfs=foo
all four paths report filter=lfs:
normal.bin: filter -> [Value(ValueRef([108, 102, 115]))] # "lfs"
special.bin: filter -> [Value(ValueRef([108, 102, 115]))]
unspec.bin: filter -> [Value(ValueRef([108, 102, 115]))]
valued.bin: filter -> [Value(ValueRef([108, 102, 115]))]
The cause appears to be in Outcome::fill_attributes (gix-attributes/src/search/outcome.rs): after recording the match, sub-attributes are pushed onto the work stack gated only on is_macro, without consulting the matched assignment's state:
let is_macro = !slot.macro_attributes.is_empty();
...
if is_macro {
// TODO(borrowchk): ...
let slot = &self.matches_by_id[id.0];
self.attrs_stack.extend(
slot.macro_attributes
.iter()
.filter(|attr| self.matches_by_id[attr.id.0].r#match.is_none())
.map(|attr| (attr.id, attr.inner.clone(), Some(id))),
);
}
Reproduced with gix-attributes 0.33.0 and 0.33.1.
Expected behavior 🤔
A macro's sub-assignments should only be applied when the matching rule sets the macro (state Set). For -mylfs, !mylfs, and mylfs=foo, filter should remain unspecified, matching git.
Git behavior
$ git check-attr filter -- normal.bin special.bin unspec.bin valued.bin
normal.bin: filter: lfs
special.bin: filter: unspecified
unspec.bin: filter: unspecified
valued.bin: filter: unspecified
(git 2.45.2.) git's attr.c gates macro expansion on the value being true, in fill_one:
if (item->macro && item->value == ATTR__TRUE)
attr_state_queue_push(&todo, item->macro);
Steps to reproduce 🕹
[dependencies]
gix-attributes = "0.33.1"
gix-glob = "0.26"
use gix_attributes::search::{MetadataCollection, Outcome};
use gix_attributes::Search;
use gix_glob::pattern::Case;
fn main() {
let attributes =
b"[attr]mylfs filter=lfs\n*.bin mylfs\nspecial.bin -mylfs\nunspec.bin !mylfs\nvalued.bin mylfs=foo\n";
let mut search = Search::default();
let mut collection = MetadataCollection::default();
search.add_patterns_buffer(
attributes,
".gitattributes".into(),
Some(std::path::Path::new("")),
&mut collection,
true, /* allow macros */
);
for path in ["normal.bin", "special.bin", "unspec.bin", "valued.bin"] {
let mut out = Outcome::default();
out.initialize_with_selection(&collection, ["filter"]);
search.pattern_matching_relative_path(path.into(), Case::Sensitive, Some(false), &mut out);
let filter = out
.iter_selected()
.map(|m| format!("{:?}", m.assignment.state))
.collect::<Vec<_>>()
.join(", ");
println!("{path}: filter -> [{filter}]");
}
}
Context: found while implementing attribute-based Git LFS detection (deciding filter=lfs for paths from tree-stored .gitattributes), where the opt-out pattern special.bin -lfs ends up smudging files git would leave untouched.
Current behavior 😯
gix_attributes::Searchexpands a macro's sub-assignments whenever a pattern referencing the macro matches, regardless of the assignment's state. Withall four paths report
filter=lfs:The cause appears to be in
Outcome::fill_attributes(gix-attributes/src/search/outcome.rs): after recording the match, sub-attributes are pushed onto the work stack gated only onis_macro, without consulting the matched assignment's state:Reproduced with gix-attributes 0.33.0 and 0.33.1.
Expected behavior 🤔
A macro's sub-assignments should only be applied when the matching rule sets the macro (state
Set). For-mylfs,!mylfs, andmylfs=foo,filtershould remain unspecified, matching git.Git behavior
(git 2.45.2.) git's
attr.cgates macro expansion on the value being true, infill_one:Steps to reproduce 🕹
Context: found while implementing attribute-based Git LFS detection (deciding
filter=lfsfor paths from tree-stored.gitattributes), where the opt-out patternspecial.bin -lfsends up smudging files git would leave untouched.