From 0354fefad4ca8cbe65e3da01835f8b0f2a205503 Mon Sep 17 00:00:00 2001 From: James Trew <66286082+jamestrew@users.noreply.github.com> Date: Thu, 14 Aug 2025 14:24:50 -0400 Subject: [PATCH] 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. --- internal/fsext/ls.go | 12 +++++-- internal/fsext/ls_test.go | 66 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 3 deletions(-) create mode 100644 internal/fsext/ls_test.go diff --git a/internal/fsext/ls.go b/internal/fsext/ls.go index e691c148ca880a17aed410f072fa1cb3078fc2b4..b18e1ef6a5257b03d33ba5a1fca009d7925ff41e 100644 --- a/internal/fsext/ls.go +++ b/internal/fsext/ls.go @@ -125,18 +125,24 @@ func (dl *directoryLister) shouldIgnore(path string, ignorePatterns []string) bo } } + // Don't apply gitignore rules to the root directory itself + // In gitignore semantics, patterns don't apply to the repo root + if path == dl.rootPath { + return false + } + relPath, err := filepath.Rel(dl.rootPath, path) if err != nil { relPath = path } if commonIgnorePatterns().MatchesPath(relPath) { - slog.Debug("ingoring common pattern", "path", relPath) + slog.Debug("ignoring common pattern", "path", relPath) return true } if dl.getIgnore(filepath.Dir(path)).MatchesPath(relPath) { - slog.Debug("ingoring dir pattern", "path", relPath, "dir", filepath.Dir(path)) + slog.Debug("ignoring dir pattern", "path", relPath, "dir", filepath.Dir(path)) return true } @@ -145,7 +151,7 @@ func (dl *directoryLister) shouldIgnore(path string, ignorePatterns []string) bo } if homeIgnore().MatchesPath(relPath) { - slog.Debug("ingoring home dir pattern", "path", relPath) + slog.Debug("ignoring home dir pattern", "path", relPath) return true } diff --git a/internal/fsext/ls_test.go b/internal/fsext/ls_test.go new file mode 100644 index 0000000000000000000000000000000000000000..a74ca3276c9af0edac6adbe1bd6e367d952af492 --- /dev/null +++ b/internal/fsext/ls_test.go @@ -0,0 +1,66 @@ +package fsext + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func chdir(t *testing.T, dir string) { + original, err := os.Getwd() + require.NoError(t, err) + + err = os.Chdir(dir) + require.NoError(t, err) + + t.Cleanup(func() { + err := os.Chdir(original) + require.NoError(t, err) + }) +} + +func TestListDirectory(t *testing.T) { + tempDir := t.TempDir() + chdir(t, tempDir) + + testFiles := map[string]string{ + "regular.txt": "content", + ".hidden": "hidden content", + ".gitignore": ".*\n*.log\n", + "subdir/file.go": "package main", + "subdir/.another": "more hidden", + "build.log": "build output", + } + + for filePath, content := range testFiles { + dir := filepath.Dir(filePath) + if dir != "." { + require.NoError(t, os.MkdirAll(dir, 0o755)) + } + + err := os.WriteFile(filePath, []byte(content), 0o644) + require.NoError(t, err) + } + + files, truncated, err := ListDirectory(".", nil, 0) + require.NoError(t, err) + assert.False(t, truncated) + assert.Equal(t, len(files), 4) + + fileSet := make(map[string]bool) + for _, file := range files { + fileSet[filepath.ToSlash(file)] = true + } + + assert.True(t, fileSet["./regular.txt"]) + assert.True(t, fileSet["./subdir/"]) + assert.True(t, fileSet["./subdir/file.go"]) + assert.True(t, fileSet["./regular.txt"]) + + assert.False(t, fileSet["./.hidden"]) + assert.False(t, fileSet["./.gitignore"]) + assert.False(t, fileSet["./build.log"]) +}