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
55 changes: 55 additions & 0 deletions file_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,61 @@ func TestFindRepositoryRootWorktree(t *testing.T) {
}
}

// TestWalkWildcardIgnoreThenReinclude reproduces issue #24: a wildcard ignore
// "/*/" that re-includes a directory via negation "!/keep/" must still walk the
// re-included directory's nested subdirectories. The leading-slash "/*/" pattern
// is anchored to the .gitignore directory and only matches first-level entries,
// so "keep/sub" must not be ignored (matching `git ls-files`).
func TestWalkWildcardIgnoreThenReinclude(t *testing.T) {
tmp := t.TempDir()

if err := os.WriteFile(filepath.Join(tmp, ".gitignore"), []byte("/*/\n!/keep/\n"), 0o644); err != nil {
t.Fatal(err)
}
if err := os.MkdirAll(filepath.Join(tmp, "keep", "sub"), 0o755); err != nil {
t.Fatal(err)
}
if err := os.MkdirAll(filepath.Join(tmp, "drop"), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(tmp, "keep", "a.rs"), []byte("a"), 0o644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(tmp, "keep", "sub", "c.rs"), []byte("c"), 0o644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(tmp, "drop", "d.rs"), []byte("d"), 0o644); err != nil {
t.Fatal(err)
}

fileListQueue := make(chan *File, 1000)
walker := NewFileWalker(tmp, fileListQueue)
if err := walker.Start(); err != nil {
t.Fatal(err)
}

got := map[string]bool{}
for f := range fileListQueue {
rel, err := filepath.Rel(tmp, f.Location)
if err != nil {
t.Fatal(err)
}
got[filepath.ToSlash(rel)] = true
}

// Expected matches `git ls-files`: keep/a.rs and keep/sub/c.rs kept,
// everything under drop/ ignored.
if !got["keep/a.rs"] {
t.Errorf("expected keep/a.rs to be walked, got %v", got)
}
if !got["keep/sub/c.rs"] {
t.Errorf("expected keep/sub/c.rs to be walked (issue #24), got %v", got)
}
if got["drop/d.rs"] {
t.Errorf("expected drop/d.rs to be ignored by /*/, got %v", got)
}
}

func TestNewFileWalker(t *testing.T) {
fileListQueue := make(chan *File, 10_000) // NB we set buffered to ensure we get everything
curdir, _ := os.Getwd()
Expand Down
7 changes: 7 additions & 0 deletions go-gitignore/pattern.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,13 @@ func (n *name) Match(path string, isdir bool) bool {
_target := path
if !n._anchored {
_, _target = filepath.Split(path)
} else if strings.ContainsRune(_target, '/') {
// an anchored name pattern is a single path component anchored to the
// base directory, so it can only ever match a single-segment path. A
// multi-segment target (e.g. "keep/sub" against "/*/") must not match,
// otherwise glob patterns such as "*" would incorrectly span the '/'
// separator and ignore nested directories that git keeps.
return false
}

// fast-path dispatch avoids expensive fnmatch for simple patterns
Expand Down
7 changes: 4 additions & 3 deletions go-gitignore/pattern_opt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,10 +135,11 @@ func TestNamePatternSuffixMatch(t *testing.T) {
// TestNamePatternSuffixMatchAnchored tests anchored suffix patterns.
func TestNamePatternSuffixMatchAnchored(t *testing.T) {
runNameTests(t, []nameMatchTest{
// anchored suffix: matches full relative path with HasSuffix
// note: name patterns use fnmatch flags=0, so * matches through /
// an anchored name pattern is a single path component anchored to the
// base directory, so it only matches single-segment paths and never
// spans the '/' separator (matching git's behaviour for "/*.o")
{"/*.o", "foo.o", false, true},
{"/*.o", "src/foo.o", false, true}, // * matches "src/foo" (no FNM_PATHNAME)
{"/*.o", "src/foo.o", false, false}, // anchored: must not match nested
})
}

Expand Down
Loading