@@ -1,14 +1,13 @@
11package fsext
22
33import (
4− "bytes"
5− "io"
64 "log/slog"
75 "os"
86 "path/filepath"
97 "strings"
108
119 "github.com/charlievieth/fastwalk"
10+ "github.com/charmbracelet/crush/internal/config"
1211 "github.com/charmbracelet/crush/internal/csync"
1312 ignore "github.com/sabhiram/go-gitignore"
1413)
@@ -77,65 +76,104 @@ type directoryLister struct {
7776}
7877
7978func NewDirectoryLister(rootPath string) *directoryLister {
80− return &directoryLister{
79+ dl := &directoryLister{
8180 rootPath: rootPath,
8281 ignores: csync.NewMap[string, ignore.IgnoreParser](),
8382 }
83+ dl.getIgnore(rootPath)
84+ dl.ignores.GetOrSet("~", func() ignore.IgnoreParser {
85+ home := config.HomeDir()
86+ var lines []string
87+ for _, name := range []string{
88+ filepath.Join(home, ".gitignore"),
89+ filepath.Join(home, ".config", "git", "ignore"),
90+ filepath.Join(home, ".config", "crush", "ignore"),
91+ } {
92+ if bts, err := os.ReadFile(name); err == nil {
93+ lines = append(lines, strings.Split(string(bts), "\n")...)
94+ }
95+ }
96+ return ignore.CompileIgnoreLines(lines...)
97+ })
98+ return dl
8499}
85100
101+// git checks, in order:
102+// - ./.gitignore, ../.gitignore, etc, until repo root
103+// ~/.config/git/ignore
104+// ~/.gitignore
105+//
106+// This will do the following:
107+// - the given ignorePatterns
108+// - [commonIgnorePatterns]
109+// - ./.gitignore, ../.gitignore, etc, until dl.rootPath
110+// - ./.crushignore, ../.crushignore, etc, until dl.rootPath
111+// ~/.config/git/ignore
112+// ~/.gitignore
113+// ~/.config/crush/ignore
86114func (dl *directoryLister) shouldIgnore(path string, ignorePatterns []string) bool {
115+ if len(ignorePatterns) > 0 {
116+ base := filepath.Base(path)
117+ for _, pattern := range ignorePatterns {
118+ if matched, err := filepath.Match(pattern, base); err == nil && matched {
119+ return true
120+ }
121+ }
122+ }
123+
87124 relPath, err := filepath.Rel(dl.rootPath, path)
88125 if err != nil {
89126 relPath = path
90127 }
91128
92− base := filepath.Base(path)
93− for _, pattern := range ignorePatterns {
94− matched, err := filepath.Match(pattern, base)
95− if err == nil && matched {
96− slog.Info("ignoring path", "path", path)
97− return true
98− }
129+ if commonIgnorePatterns.MatchesPath(relPath) {
130+ slog.Debug("ingoring common pattern", "path", relPath)
131+ return true
99132 }
100133
101− if commonIgnorePatterns.MatchesPath(relPath) || dl.getIgnore(path).MatchesPath(relPath) {
102− slog.Info("ignoring path", "path", path)
134+ if dl.getIgnore(filepath.Dir(path)).MatchesPath(relPath) {
135+ slog.Debug("ingoring dir pattern", "path", relPath, "dir", filepath.Dir(path))
103136 return true
104137 }
105138
106− parent := filepath.Dir(path)
107− for {
139+ if dl.checkParentIgnores(relPath) {
140+ return true
141+ }
142+
143+ if dl.getIgnore("~").MatchesPath(relPath) {
144+ slog.Debug("ingoring home dir pattern", "path", relPath)
145+ return true
146+ }
147+
148+ return false
149+}
150+
151+func (dl *directoryLister) checkParentIgnores(path string) bool {
152+ parent := filepath.Dir(filepath.Dir(path))
153+ for parent != dl.rootPath && parent != "." && path != "." {
108154 if dl.getIgnore(parent).MatchesPath(path) {
109− slog.Info("ignoring path", "path", path, "parent", parent)
155+ slog.Debug("ingoring parent dir pattern", "path", path, "dir", parent)
110156 return true
111157 }
112− if parent == "/" || parent == "." { // TODO: windows
113− return false
114− }
115158 parent = filepath.Dir(parent)
116159 }
160+ return false
117161}
118162
119163func (dl *directoryLister) getIgnore(path string) ignore.IgnoreParser {
120164 return dl.ignores.GetOrSet(path, func() ignore.IgnoreParser {
121− var b bytes.Buffer
165+ var lines []string
122166 for _, ign := range []string{".crushignore", ".gitignore"} {
123− p := filepath.Join(path, ign)
124− if _, err := os.Stat(p); err == nil {
125− slog.Info("loading ignore file", "path", p)
126− f, err := os.Open(p)
127− if err != nil {
128− _ = f.Close()
129− slog.Error("Failed to open ignore file", "path", p, "error", err)
130− continue
131− }
132− if _, err := io.Copy(&b, f); err != nil {
133− slog.Error("Failed to read ignore file", "path", p, "error", err)
134− }
135− _ = f.Close()
167+ name := filepath.Join(path, ign)
168+ if content, err := os.ReadFile(name); err == nil {
169+ lines = append(lines, strings.Split(string(content), "\n")...)
136170 }
137171 }
138− return ignore.CompileIgnoreLines(strings.Split(b.String(), "\n")...)
172+ if len(lines) == 0 {
173+ // Return a no-op parser to avoid nil checks
174+ return ignore.CompileIgnoreLines()
175+ }
176+ return ignore.CompileIgnoreLines(lines...)
139177 })
140178}
141179