@@ -1,14 +1,13 @@
1package fsext
1package fsext
8 "path/filepath"
6 "path/filepath"
11 "github.com/charlievieth/fastwalk"
9 "github.com/charlievieth/fastwalk"
10+ "github.com/charmbracelet/crush/internal/config"
12 "github.com/charmbracelet/crush/internal/csync"
11 "github.com/charmbracelet/crush/internal/csync"
13 ignore "github.com/sabhiram/go-gitignore"
12 ignore "github.com/sabhiram/go-gitignore"
@@ -77,65 +76,104 @@ type directoryLister struct {
79func NewDirectoryLister(rootPath string) *directoryLister {
78func NewDirectoryLister(rootPath string) *directoryLister {
80− return &directoryLister{
79+ dl := &directoryLister{
81 rootPath: rootPath,
80 rootPath: rootPath,
82 ignores: csync.NewMap[string, ignore.IgnoreParser](),
81 ignores: csync.NewMap[string, ignore.IgnoreParser](),
83+ dl.getIgnore(rootPath)
84+ dl.ignores.GetOrSet("~", func() ignore.IgnoreParser {
85+ home := config.HomeDir()
87+ for _, name := range []string{
88+ filepath.Join(home, ".gitignore"),
89+ filepath.Join(home, ".config", "git", "ignore"),
90+ filepath.Join(home, ".config", "crush", "ignore"),
92+ if bts, err := os.ReadFile(name); err == nil {
93+ lines = append(lines, strings.Split(string(bts), "\n")...)
96+ return ignore.CompileIgnoreLines(lines...)
101+// git checks, in order:
102+// - ./.gitignore, ../.gitignore, etc, until repo root
103+// ~/.config/git/ignore
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
113+// ~/.config/crush/ignore
86func (dl *directoryLister) shouldIgnore(path string, ignorePatterns []string) bool {
114func (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 {
87 relPath, err := filepath.Rel(dl.rootPath, path)
124 relPath, err := filepath.Rel(dl.rootPath, path)
88 if err != nil {
125 if err != nil {
89 relPath = path
126 relPath = path
92− base := filepath.Base(path)
129+ if commonIgnorePatterns.MatchesPath(relPath) {
93− for _, pattern := range ignorePatterns {
130+ slog.Debug("ingoring common pattern", "path", relPath)
94− matched, err := filepath.Match(pattern, base)
95− if err == nil && matched {
96− slog.Info("ignoring path", "path", path)
97− return true
131+ return true
101− if commonIgnorePatterns.MatchesPath(relPath) || dl.getIgnore(path).MatchesPath(relPath) {
134+ if dl.getIgnore(filepath.Dir(path)).MatchesPath(relPath) {
102− slog.Info("ignoring path", "path", path)
135+ slog.Debug("ingoring dir pattern", "path", relPath, "dir", filepath.Dir(path))
103 return true
136 return true
139+ if dl.checkParentIgnores(relPath) {
143+ if dl.getIgnore("~").MatchesPath(relPath) {
144+ slog.Debug("ingoring home dir pattern", "path", relPath)
151+func (dl *directoryLister) checkParentIgnores(path string) bool {
106− parent := filepath.Dir(path)
152+ parent := filepath.Dir(filepath.Dir(path))
107− for {
153+ for parent != dl.rootPath && parent != "." && path != "." {
108 if dl.getIgnore(parent).MatchesPath(path) {
154 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)
110 return true
156 return true
112− if parent == "/" || parent == "." { // TODO: windows
115 parent = filepath.Dir(parent)
158 parent = filepath.Dir(parent)
119func (dl *directoryLister) getIgnore(path string) ignore.IgnoreParser {
163func (dl *directoryLister) getIgnore(path string) ignore.IgnoreParser {
120 return dl.ignores.GetOrSet(path, func() ignore.IgnoreParser {
164 return dl.ignores.GetOrSet(path, func() ignore.IgnoreParser {
121− var b bytes.Buffer
165+ var lines []string
122 for _, ign := range []string{".crushignore", ".gitignore"} {
166 for _, ign := range []string{".crushignore", ".gitignore"} {
123− p := filepath.Join(path, ign)
167+ name := filepath.Join(path, ign)
124− if _, err := os.Stat(p); err == nil {
168+ if content, err := os.ReadFile(name); err == nil {
125− slog.Info("loading ignore file", "path", p)
169+ lines = append(lines, strings.Split(string(content), "\n")...)
126− f, err := os.Open(p)
129− slog.Error("Failed to open ignore file", "path", p, "error", err)
132− if _, err := io.Copy(&b, f); err != nil {
133− slog.Error("Failed to read ignore file", "path", p, "error", err)
172+ if len(lines) == 0 {
173+ // Return a no-op parser to avoid nil checks
138− return ignore.CompileIgnoreLines(strings.Split(b.String(), "\n")...)
174+ return ignore.CompileIgnoreLines()
176+ return ignore.CompileIgnoreLines(lines...)