1package fsext
2
3import (
4 "os"
5 "path/filepath"
6
7 "github.com/charlievieth/fastwalk"
8 ignore "github.com/sabhiram/go-gitignore"
9)
10
11// CommonIgnorePatterns contains commonly ignored files and directories
12var CommonIgnorePatterns = []string{
13 // Version control
14 ".git",
15 ".svn",
16 ".hg",
17 ".bzr",
18
19 // IDE and editor files
20 ".vscode",
21 ".idea",
22 "*.swp",
23 "*.swo",
24 "*~",
25 ".DS_Store",
26 "Thumbs.db",
27
28 // Build artifacts and dependencies
29 "node_modules",
30 "target",
31 "build",
32 "dist",
33 "out",
34 "bin",
35 "obj",
36 "*.o",
37 "*.so",
38 "*.dylib",
39 "*.dll",
40 "*.exe",
41
42 // Logs and temporary files
43 "*.log",
44 "*.tmp",
45 "*.temp",
46 ".cache",
47 ".tmp",
48
49 // Language-specific
50 "__pycache__",
51 "*.pyc",
52 "*.pyo",
53 ".pytest_cache",
54 "vendor",
55 "Cargo.lock",
56 "package-lock.json",
57 "yarn.lock",
58 "pnpm-lock.yaml",
59
60 // OS generated files
61 ".Trash",
62 ".Spotlight-V100",
63 ".fseventsd",
64
65 // Crush
66 ".crush",
67}
68
69type DirectoryLister struct {
70 gitignore *ignore.GitIgnore
71 crushignore *ignore.GitIgnore
72 commonIgnore *ignore.GitIgnore
73 rootPath string
74}
75
76func NewDirectoryLister(rootPath string) *DirectoryLister {
77 dl := &DirectoryLister{
78 rootPath: rootPath,
79 }
80
81 // Load gitignore if it exists
82 gitignorePath := filepath.Join(rootPath, ".gitignore")
83 if _, err := os.Stat(gitignorePath); err == nil {
84 if gi, err := ignore.CompileIgnoreFile(gitignorePath); err == nil {
85 dl.gitignore = gi
86 }
87 }
88
89 // Load crushignore if it exists
90 crushignorePath := filepath.Join(rootPath, ".crushignore")
91 if _, err := os.Stat(crushignorePath); err == nil {
92 if ci, err := ignore.CompileIgnoreFile(crushignorePath); err == nil {
93 dl.crushignore = ci
94 }
95 }
96
97 // Create common ignore patterns
98 dl.commonIgnore = ignore.CompileIgnoreLines(CommonIgnorePatterns...)
99
100 return dl
101}
102
103func (dl *DirectoryLister) shouldIgnore(path string, ignorePatterns []string) bool {
104 relPath, err := filepath.Rel(dl.rootPath, path)
105 if err != nil {
106 relPath = path
107 }
108
109 // Check common ignore patterns
110 if dl.commonIgnore.MatchesPath(relPath) {
111 return true
112 }
113
114 // Check gitignore patterns if available
115 if dl.gitignore != nil && dl.gitignore.MatchesPath(relPath) {
116 return true
117 }
118
119 // Check crushignore patterns if available
120 if dl.crushignore != nil && dl.crushignore.MatchesPath(relPath) {
121 return true
122 }
123
124 base := filepath.Base(path)
125
126 for _, pattern := range ignorePatterns {
127 matched, err := filepath.Match(pattern, base)
128 if err == nil && matched {
129 return true
130 }
131 }
132 return false
133}
134
135// ListDirectory lists files and directories in the specified path,
136func ListDirectory(initialPath string, ignorePatterns []string, limit int) ([]string, bool, error) {
137 var results []string
138 truncated := false
139 dl := NewDirectoryLister(initialPath)
140
141 conf := fastwalk.Config{
142 Follow: true,
143 // Use forward slashes when running a Windows binary under WSL or MSYS
144 ToSlash: fastwalk.DefaultToSlash(),
145 Sort: fastwalk.SortDirsFirst,
146 }
147 err := fastwalk.Walk(&conf, initialPath, func(path string, d os.DirEntry, err error) error {
148 if err != nil {
149 return nil // Skip files we don't have permission to access
150 }
151
152 if dl.shouldIgnore(path, ignorePatterns) {
153 if d.IsDir() {
154 return filepath.SkipDir
155 }
156 return nil
157 }
158
159 if path != initialPath {
160 if d.IsDir() {
161 path = path + string(filepath.Separator)
162 }
163 results = append(results, path)
164 }
165
166 if limit > 0 && len(results) >= limit {
167 truncated = true
168 return filepath.SkipAll
169 }
170
171 return nil
172 })
173 if err != nil && len(results) == 0 {
174 return nil, truncated, err
175 }
176
177 return results, truncated, nil
178}