@@ -1,137 +1,186 @@
1package fsext
1package fsext
5 "path/filepath"
6 "path/filepath"
7 "github.com/charlievieth/fastwalk"
10 "github.com/charlievieth/fastwalk"
11+ "github.com/charmbracelet/crush/internal/config"
12+ "github.com/charmbracelet/crush/internal/csync"
8 ignore "github.com/sabhiram/go-gitignore"
13 ignore "github.com/sabhiram/go-gitignore"
11−// CommonIgnorePatterns contains commonly ignored files and directories
16+// commonIgnorePatterns contains commonly ignored files and directories
12−var CommonIgnorePatterns = []string{
17+var commonIgnorePatterns = sync.OnceValue(func() ignore.IgnoreParser {
18+ return ignore.CompileIgnoreLines(
13− // Version control
19+ // Version control
19− // IDE and editor files
25+ // IDE and editor files
20− ".vscode",
26+ ".vscode",
25− ".DS_Store",
31+ ".DS_Store",
26− "Thumbs.db",
32+ "Thumbs.db",
28− // Build artifacts and dependencies
34+ // Build artifacts and dependencies
29− "node_modules",
35+ "node_modules",
30− "target",
36+ "target",
38− "*.dylib",
44+ "*.dylib",
42− // Logs and temporary files
48+ // Logs and temporary files
45− "*.temp",
51+ "*.temp",
46− ".cache",
52+ ".cache",
49− // Language-specific
55+ // Language-specific
50− "__pycache__",
56+ "__pycache__",
53− ".pytest_cache",
59+ ".pytest_cache",
54− "vendor",
60+ "vendor",
55− "Cargo.lock",
61+ "Cargo.lock",
56− "package-lock.json",
62+ "package-lock.json",
57− "yarn.lock",
63+ "yarn.lock",
58− "pnpm-lock.yaml",
64+ "pnpm-lock.yaml",
60− // OS generated files
66+ // OS generated files
61− ".Trash",
67+ ".Trash",
62− ".Spotlight-V100",
68+ ".Spotlight-V100",
63− ".fseventsd",
69+ ".fseventsd",
66− ".crush",
72+ ".crush",
76+var homeIgnore = sync.OnceValue(func() ignore.IgnoreParser {
77+ home := config.HomeDir()
79+ for _, name := range []string{
80+ filepath.Join(home, ".gitignore"),
81+ filepath.Join(home, ".config", "git", "ignore"),
82+ filepath.Join(home, ".config", "crush", "ignore"),
84+ if bts, err := os.ReadFile(name); err == nil {
85+ lines = append(lines, strings.Split(string(bts), "\n")...)
88+ return ignore.CompileIgnoreLines(lines...)
69−type DirectoryLister struct {
91+type directoryLister struct {
70− gitignore *ignore.GitIgnore
92+ ignores *csync.Map[string, ignore.IgnoreParser]
71− crushignore *ignore.GitIgnore
72− commonIgnore *ignore.GitIgnore
73− rootPath string
93+ rootPath string
76−func NewDirectoryLister(rootPath string) *DirectoryLister {
96+func NewDirectoryLister(rootPath string) *directoryLister {
77− dl := &DirectoryLister{
97+ dl := &directoryLister{
78 rootPath: rootPath,
98 rootPath: rootPath,
99+ ignores: csync.NewMap[string, ignore.IgnoreParser](),
101+ dl.getIgnore(rootPath)
81− // Load gitignore if it exists
105+// git checks, in order:
82− gitignorePath := filepath.Join(rootPath, ".gitignore")
106+// - ./.gitignore, ../.gitignore, etc, until repo root
83− if _, err := os.Stat(gitignorePath); err == nil {
107+// ~/.config/git/ignore
84− if gi, err := ignore.CompileIgnoreFile(gitignorePath); err == nil {
108+// ~/.gitignore
85− dl.gitignore = gi
109+//
86− }
110+// This will do the following:
87− }
111+// - the given ignorePatterns
88−
112+// - [commonIgnorePatterns]
89− // Load crushignore if it exists
113+// - ./.gitignore, ../.gitignore, etc, until dl.rootPath
90− crushignorePath := filepath.Join(rootPath, ".crushignore")
114+// - ./.crushignore, ../.crushignore, etc, until dl.rootPath
91− if _, err := os.Stat(crushignorePath); err == nil {
115+// ~/.config/git/ignore
92− if ci, err := ignore.CompileIgnoreFile(crushignorePath); err == nil {
116+// ~/.gitignore
93− dl.crushignore = ci
117+// ~/.config/crush/ignore
118+func (dl *directoryLister) shouldIgnore(path string, ignorePatterns []string) bool {
119+ if len(ignorePatterns) > 0 {
120+ base := filepath.Base(path)
121+ for _, pattern := range ignorePatterns {
122+ if matched, err := filepath.Match(pattern, base); err == nil && matched {
97− // Create common ignore patterns
98− dl.commonIgnore = ignore.CompileIgnoreLines(CommonIgnorePatterns...)
103−func (dl *DirectoryLister) shouldIgnore(path string, ignorePatterns []string) bool {
104 relPath, err := filepath.Rel(dl.rootPath, path)
128 relPath, err := filepath.Rel(dl.rootPath, path)
105 if err != nil {
129 if err != nil {
106 relPath = path
130 relPath = path
109− // Check common ignore patterns
110− if dl.commonIgnore.MatchesPath(relPath) {
133+ if commonIgnorePatterns().MatchesPath(relPath) {
134+ slog.Debug("ingoring common pattern", "path", relPath)
111 return true
135 return true
114− // Check gitignore patterns if available
115− if dl.gitignore != nil && dl.gitignore.MatchesPath(relPath) {
138+ if dl.getIgnore(filepath.Dir(path)).MatchesPath(relPath) {
139+ slog.Debug("ingoring dir pattern", "path", relPath, "dir", filepath.Dir(path))
116 return true
140 return true
119− // Check crushignore patterns if available
143+ if dl.checkParentIgnores(relPath) {
120− if dl.crushignore != nil && dl.crushignore.MatchesPath(relPath) {
121 return true
144 return true
124− base := filepath.Base(path)
147+ if homeIgnore().MatchesPath(relPath) {
148+ slog.Debug("ingoring home dir pattern", "path", relPath)
126− for _, pattern := range ignorePatterns {
155+func (dl *directoryLister) checkParentIgnores(path string) bool {
127− matched, err := filepath.Match(pattern, base)
156+ parent := filepath.Dir(filepath.Dir(path))
128− if err == nil && matched {
157+ for parent != dl.rootPath && parent != "." && path != "." {
158+ if dl.getIgnore(parent).MatchesPath(path) {
159+ slog.Debug("ingoring parent dir pattern", "path", path, "dir", parent)
129 return true
160 return true
162+ parent = filepath.Dir(parent)
132 return false
164 return false
167+func (dl *directoryLister) getIgnore(path string) ignore.IgnoreParser {
168+ return dl.ignores.GetOrSet(path, func() ignore.IgnoreParser {
170+ for _, ign := range []string{".crushignore", ".gitignore"} {
171+ name := filepath.Join(path, ign)
172+ if content, err := os.ReadFile(name); err == nil {
173+ lines = append(lines, strings.Split(string(content), "\n")...)
176+ if len(lines) == 0 {
177+ // Return a no-op parser to avoid nil checks
178+ return ignore.CompileIgnoreLines()
180+ return ignore.CompileIgnoreLines(lines...)
135// ListDirectory lists files and directories in the specified path,
184// ListDirectory lists files and directories in the specified path,
136func ListDirectory(initialPath string, ignorePatterns []string, limit int) ([]string, bool, error) {
185func ListDirectory(initialPath string, ignorePatterns []string, limit int) ([]string, bool, error) {
137 var results []string
186 var results []string
@@ -144,6 +193,7 @@ func ListDirectory(initialPath string, ignorePatterns []string, limit int) ([]st
144 ToSlash: fastwalk.DefaultToSlash(),
193 ToSlash: fastwalk.DefaultToSlash(),
145 Sort: fastwalk.SortDirsFirst,
194 Sort: fastwalk.SortDirsFirst,
147 err := fastwalk.Walk(&conf, initialPath, func(path string, d os.DirEntry, err error) error {
197 err := fastwalk.Walk(&conf, initialPath, func(path string, d os.DirEntry, err error) error {
148 if err != nil {
198 if err != nil {
149 return nil // Skip files we don't have permission to access
199 return nil // Skip files we don't have permission to access