on main help sign in
fix(fsext): prevent `.*` on gitignore from ignoring entire root dir (#766)

Fixed critical bug where gitignore patterns like `.*` would match the root directory itself, causing `filepath.SkipDir` to abort entire file listing. The fix prevents gitignore rules from applying to the scan root directory, matching Git's actual behavior.

commit0354fefad4ca8cbe65e3da01835f8b0f2a205503 authorJames Trew <66286082+jamestrew@users.noreply.github.com> dateThu 14 Aug 2025, 14:24 (1 year ago) parent659d81a
2 files changed+75−3
internal/fsext/ls.go+9−3 internal/fsext/ls_test.go+66
internal/fsext/ls.go+9−3
@@ -125,18 +125,24 @@ func (dl *directoryLister) shouldIgnore(path string, ignorePatterns []string) bo
125125 }
126126 }
127127
128+ // Don't apply gitignore rules to the root directory itself
129+ // In gitignore semantics, patterns don't apply to the repo root
130+ if path == dl.rootPath {
131+ return false
132+ }
133+
128134 relPath, err := filepath.Rel(dl.rootPath, path)
129135 if err != nil {
130136 relPath = path
131137 }
132138
133139 if commonIgnorePatterns().MatchesPath(relPath) {
134− slog.Debug("ingoring common pattern", "path", relPath)
140+ slog.Debug("ignoring common pattern", "path", relPath)
135141 return true
136142 }
137143
138144 if dl.getIgnore(filepath.Dir(path)).MatchesPath(relPath) {
139− slog.Debug("ingoring dir pattern", "path", relPath, "dir", filepath.Dir(path))
145+ slog.Debug("ignoring dir pattern", "path", relPath, "dir", filepath.Dir(path))
140146 return true
141147 }
142148
@@ -145,7 +151,7 @@ func (dl *directoryLister) shouldIgnore(path string, ignorePatterns []string) bo
145151 }
146152
147153 if homeIgnore().MatchesPath(relPath) {
148− slog.Debug("ingoring home dir pattern", "path", relPath)
154+ slog.Debug("ignoring home dir pattern", "path", relPath)
149155 return true
150156 }
151157
internal/fsext/ls_test.gonew file+66
@@ -0,0 +1,66 @@
1+package fsext
2+
3+import (
4+ "os"
5+ "path/filepath"
6+ "testing"
7+
8+ "github.com/stretchr/testify/assert"
9+ "github.com/stretchr/testify/require"
10+)
11+
12+func chdir(t *testing.T, dir string) {
13+ original, err := os.Getwd()
14+ require.NoError(t, err)
15+
16+ err = os.Chdir(dir)
17+ require.NoError(t, err)
18+
19+ t.Cleanup(func() {
20+ err := os.Chdir(original)
21+ require.NoError(t, err)
22+ })
23+}
24+
25+func TestListDirectory(t *testing.T) {
26+ tempDir := t.TempDir()
27+ chdir(t, tempDir)
28+
29+ testFiles := map[string]string{
30+ "regular.txt": "content",
31+ ".hidden": "hidden content",
32+ ".gitignore": ".*\n*.log\n",
33+ "subdir/file.go": "package main",
34+ "subdir/.another": "more hidden",
35+ "build.log": "build output",
36+ }
37+
38+ for filePath, content := range testFiles {
39+ dir := filepath.Dir(filePath)
40+ if dir != "." {
41+ require.NoError(t, os.MkdirAll(dir, 0o755))
42+ }
43+
44+ err := os.WriteFile(filePath, []byte(content), 0o644)
45+ require.NoError(t, err)
46+ }
47+
48+ files, truncated, err := ListDirectory(".", nil, 0)
49+ require.NoError(t, err)
50+ assert.False(t, truncated)
51+ assert.Equal(t, len(files), 4)
52+
53+ fileSet := make(map[string]bool)
54+ for _, file := range files {
55+ fileSet[filepath.ToSlash(file)] = true
56+ }
57+
58+ assert.True(t, fileSet["./regular.txt"])
59+ assert.True(t, fileSet["./subdir/"])
60+ assert.True(t, fileSet["./subdir/file.go"])
61+ assert.True(t, fileSet["./regular.txt"])
62+
63+ assert.False(t, fileSet["./.hidden"])
64+ assert.False(t, fileSet["./.gitignore"])
65+ assert.False(t, fileSet["./build.log"])
66+}