From fbe879d4b56d1f95fd02b36b986def6afe8eb0f8 Mon Sep 17 00:00:00 2001 From: Carlos Alexandro Becker Date: Thu, 7 Aug 2025 14:01:36 -0300 Subject: [PATCH] feat: improve .crushignore and .gitignore Signed-off-by: Carlos Alexandro Becker --- internal/fsext/ignore_test.go | 20 ++----- internal/fsext/ls.go | 104 +++++++++++++++++++++++----------- 2 files changed, 75 insertions(+), 49 deletions(-) diff --git a/internal/fsext/ignore_test.go b/internal/fsext/ignore_test.go index 48a56667d0f3776c4054e8abbeff8eaeb67c9dfe..c2490a062d2e55fc96dda78c597fc867465f032e 100644 --- a/internal/fsext/ignore_test.go +++ b/internal/fsext/ignore_test.go @@ -25,20 +25,8 @@ func TestCrushIgnore(t *testing.T) { // Create a .crushignore file that ignores .log files require.NoError(t, os.WriteFile(".crushignore", []byte("*.log\n"), 0o644)) - // Test DirectoryLister - t.Run("DirectoryLister respects .crushignore", func(t *testing.T) { - dl := NewDirectoryLister(tempDir) - - // Test that .log files are ignored - require.True(t, dl.gitignore == nil, "gitignore should be nil") - require.NotNil(t, dl.crushignore, "crushignore should not be nil") - }) - - // Test FastGlobWalker - t.Run("FastGlobWalker respects .crushignore", func(t *testing.T) { - walker := NewFastGlobWalker(tempDir) - - require.True(t, walker.gitignore == nil, "gitignore should be nil") - require.NotNil(t, walker.crushignore, "crushignore should not be nil") - }) + dl := NewDirectoryLister(tempDir) + require.True(t, dl.shouldIgnore("test2.log", nil), ".log files should be ignored") + require.False(t, dl.shouldIgnore("test1.txt", nil), ".txt files should not be ignored") + require.True(t, dl.shouldIgnore("test3.tmp", nil), ".tmp files should be ignored by common patterns") } diff --git a/internal/fsext/ls.go b/internal/fsext/ls.go index d106e7cf106caa906f7d95f05c60f477fc078cbd..d5fd50f5c283d70ec148109b1bdef5f753cbdc5b 100644 --- a/internal/fsext/ls.go +++ b/internal/fsext/ls.go @@ -1,14 +1,13 @@ package fsext import ( - "bytes" - "io" "log/slog" "os" "path/filepath" "strings" "github.com/charlievieth/fastwalk" + "github.com/charmbracelet/crush/internal/config" "github.com/charmbracelet/crush/internal/csync" ignore "github.com/sabhiram/go-gitignore" ) @@ -77,65 +76,104 @@ type directoryLister struct { } func NewDirectoryLister(rootPath string) *directoryLister { - return &directoryLister{ + dl := &directoryLister{ rootPath: rootPath, ignores: csync.NewMap[string, ignore.IgnoreParser](), } + dl.getIgnore(rootPath) + dl.ignores.GetOrSet("~", func() ignore.IgnoreParser { + home := config.HomeDir() + var lines []string + for _, name := range []string{ + filepath.Join(home, ".gitignore"), + filepath.Join(home, ".config", "git", "ignore"), + filepath.Join(home, ".config", "crush", "ignore"), + } { + if bts, err := os.ReadFile(name); err == nil { + lines = append(lines, strings.Split(string(bts), "\n")...) + } + } + return ignore.CompileIgnoreLines(lines...) + }) + return dl } +// git checks, in order: +// - ./.gitignore, ../.gitignore, etc, until repo root +// ~/.config/git/ignore +// ~/.gitignore +// +// This will do the following: +// - the given ignorePatterns +// - [commonIgnorePatterns] +// - ./.gitignore, ../.gitignore, etc, until dl.rootPath +// - ./.crushignore, ../.crushignore, etc, until dl.rootPath +// ~/.config/git/ignore +// ~/.gitignore +// ~/.config/crush/ignore func (dl *directoryLister) shouldIgnore(path string, ignorePatterns []string) bool { + if len(ignorePatterns) > 0 { + base := filepath.Base(path) + for _, pattern := range ignorePatterns { + if matched, err := filepath.Match(pattern, base); err == nil && matched { + return true + } + } + } + relPath, err := filepath.Rel(dl.rootPath, path) if err != nil { relPath = path } - base := filepath.Base(path) - for _, pattern := range ignorePatterns { - matched, err := filepath.Match(pattern, base) - if err == nil && matched { - slog.Info("ignoring path", "path", path) - return true - } + if commonIgnorePatterns.MatchesPath(relPath) { + slog.Debug("ingoring common pattern", "path", relPath) + return true } - if commonIgnorePatterns.MatchesPath(relPath) || dl.getIgnore(path).MatchesPath(relPath) { - slog.Info("ignoring path", "path", path) + if dl.getIgnore(filepath.Dir(path)).MatchesPath(relPath) { + slog.Debug("ingoring dir pattern", "path", relPath, "dir", filepath.Dir(path)) return true } - parent := filepath.Dir(path) - for { + if dl.checkParentIgnores(relPath) { + return true + } + + if dl.getIgnore("~").MatchesPath(relPath) { + slog.Debug("ingoring home dir pattern", "path", relPath) + return true + } + + return false +} + +func (dl *directoryLister) checkParentIgnores(path string) bool { + parent := filepath.Dir(filepath.Dir(path)) + for parent != dl.rootPath && parent != "." && path != "." { if dl.getIgnore(parent).MatchesPath(path) { - slog.Info("ignoring path", "path", path, "parent", parent) + slog.Debug("ingoring parent dir pattern", "path", path, "dir", parent) return true } - if parent == "/" || parent == "." { // TODO: windows - return false - } parent = filepath.Dir(parent) } + return false } func (dl *directoryLister) getIgnore(path string) ignore.IgnoreParser { return dl.ignores.GetOrSet(path, func() ignore.IgnoreParser { - var b bytes.Buffer + var lines []string for _, ign := range []string{".crushignore", ".gitignore"} { - p := filepath.Join(path, ign) - if _, err := os.Stat(p); err == nil { - slog.Info("loading ignore file", "path", p) - f, err := os.Open(p) - if err != nil { - _ = f.Close() - slog.Error("Failed to open ignore file", "path", p, "error", err) - continue - } - if _, err := io.Copy(&b, f); err != nil { - slog.Error("Failed to read ignore file", "path", p, "error", err) - } - _ = f.Close() + name := filepath.Join(path, ign) + if content, err := os.ReadFile(name); err == nil { + lines = append(lines, strings.Split(string(content), "\n")...) } } - return ignore.CompileIgnoreLines(strings.Split(b.String(), "\n")...) + if len(lines) == 0 { + // Return a no-op parser to avoid nil checks + return ignore.CompileIgnoreLines() + } + return ignore.CompileIgnoreLines(lines...) }) }