Mats Willemsen 2 weeks ago committed by GitHub
commit ed4b3cc2c7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -85,8 +85,9 @@ func Parse(file io.Reader) (*Rules, error) {
// Ignore evaluates the file at the given path, and returns true if it should be ignored.
//
// Ignore evaluates path against the rules in order. Evaluation stops when a match
// is found. Matching a negative rule will stop evaluation.
// Ignore evaluates path against all rules in order. Later rules can override earlier ones,
// following gitignore semantics where negation patterns (starting with !) can re-include
// previously excluded files.
func (r *Rules) Ignore(path string, fi os.FileInfo) bool {
// Don't match on empty dirs.
if path == "" {
@ -99,34 +100,30 @@ func (r *Rules) Ignore(path string, fi os.FileInfo) bool {
if path == "." || path == "./" {
return false
}
// Track whether the file should be ignored. Start with false (not ignored),
// and let each matching rule update this value. The last matching rule wins.
ignored := false
for _, p := range r.patterns {
if p.match == nil {
slog.Info("this will be ignored no matcher supplied", "patterns", p.raw)
return false
}
// For negative rules, we need to capture and return non-matches,
// and continue for matches.
if p.negate {
if p.mustDir && !fi.IsDir() {
return true
}
if !p.match(path, fi) {
return true
}
continue
}
// If the rule is looking for directories, and this is not a directory,
// skip it.
if p.mustDir && !fi.IsDir() {
continue
}
if p.match(path, fi) {
return true
// For negation rules, matching means "don't ignore this file"
// For regular rules, matching means "ignore this file"
ignored = !p.negate
}
}
return false
return ignored
}
// parseRule parses a rule string and creates a pattern, which is then stored in the Rules object.

@ -112,11 +112,11 @@ func TestIgnore(t *testing.T) {
{`cargo/`, "mast/", false},
{`helm.txt/`, "helm.txt", false},
// Negation tests
// Negation tests (single pattern - negation alone doesn't ignore non-matches)
{`!helm.txt`, "helm.txt", false},
{`!helm.txt`, "tiller.txt", true},
{`!*.txt`, "cargo", true},
{`!cargo/`, "mast/", true},
{`!helm.txt`, "tiller.txt", false},
{`!*.txt`, "cargo", false},
{`!cargo/`, "mast/", false},
// Absolute path tests
{`/a.txt`, "a.txt", true},
@ -149,6 +149,87 @@ func TestAddDefaults(t *testing.T) {
}
}
func TestNegationPatterns(t *testing.T) {
// Test multi-rule scenarios with negation patterns (gitignore semantics)
tests := []struct {
name string
rules string
file string
expected bool
}{
// Basic negation: *.txt ignored, but README.txt is un-ignored
{
name: "negation re-includes previously excluded file",
rules: "*.txt\n!helm.txt",
file: "helm.txt",
expected: false, // Should NOT be ignored (negation re-includes it)
},
{
name: "file matching exclusion but not negation is ignored",
rules: "*.txt\n!helm.txt",
file: "tiller.txt",
expected: true, // Should be ignored (matches *.txt, doesn't match negation)
},
{
name: "unrelated file is not affected by negation",
rules: "*.txt\n!helm.txt",
file: "cargo",
expected: false, // Should NOT be ignored (doesn't match *.txt)
},
// Multiple negations
{
name: "multiple negations - first negated file",
rules: "*.txt\n!helm.txt\n!tiller.txt",
file: "helm.txt",
expected: false,
},
{
name: "multiple negations - second negated file",
rules: "*.txt\n!helm.txt\n!tiller.txt",
file: "tiller.txt",
expected: false,
},
{
name: "multiple negations - non-negated file still ignored",
rules: "*.txt\n!helm.txt\n!tiller.txt",
file: "rudder.txt",
expected: true,
},
// Negation pattern alone should not ignore anything
{
name: "negation alone does not ignore non-matching files",
rules: "!helm.txt",
file: "tiller.txt",
expected: false,
},
{
name: "negation alone explicitly un-ignores matching files",
rules: "!helm.txt",
file: "helm.txt",
expected: false,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
r, err := parseString(test.rules)
if err != nil {
t.Fatalf("Failed to parse rules: %s", err)
}
fi, err := os.Stat(filepath.Join(testdata, test.file))
if err != nil {
t.Fatalf("Fixture missing: %s", err)
}
result := r.Ignore(test.file, fi)
if result != test.expected {
t.Errorf("Expected Ignore(%q) to be %v for rules %q, got %v",
test.file, test.expected, test.rules, result)
}
})
}
}
func parseString(str string) (*Rules, error) {
b := bytes.NewBuffer([]byte(str))
return Parse(b)

Loading…
Cancel
Save