Skip to content

gix-attributes: macros expand even when the matching rule unsets them, diverging from git check-attr #2639

Description

@NilsIrl

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions