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 commonIgnore *ignore.GitIgnore
72 rootPath string
73}
74
75func NewDirectoryLister(rootPath string) *DirectoryLister {
76 dl := &DirectoryLister{
77 rootPath: rootPath,
78 }
79
80 // Load gitignore if it exists
81 gitignorePath := filepath.Join(rootPath, ".gitignore")
82 if _, err := os.Stat(gitignorePath); err == nil {
83 if gi, err := ignore.CompileIgnoreFile(gitignorePath); err == nil {
84 dl.gitignore = gi
85 }
86 }
87
88 // Create common ignore patterns
89 dl.commonIgnore = ignore.CompileIgnoreLines(CommonIgnorePatterns...)
90
91 return dl
92}
93
94func (dl *DirectoryLister) shouldIgnore(path string, ignorePatterns []string) bool {
95 relPath, err := filepath.Rel(dl.rootPath, path)
96 if err != nil {
97 relPath = path
98 }
99
100 // Check common ignore patterns
101 if dl.commonIgnore.MatchesPath(relPath) {
102 return true
103 }
104
105 // Check gitignore patterns if available
106 if dl.gitignore != nil && dl.gitignore.MatchesPath(relPath) {
107 return true
108 }
109
110 base := filepath.Base(path)
111
112 for _, pattern := range ignorePatterns {
113 matched, err := filepath.Match(pattern, base)
114 if err == nil && matched {
115 return true
116 }
117 }
118 return false
119}
120
121// ListDirectory lists files and directories in the specified path,
122func ListDirectory(initialPath string, ignorePatterns []string, limit int) ([]string, bool, error) {
123 var results []string
124 truncated := false
125 dl := NewDirectoryLister(initialPath)
126
127 conf := fastwalk.Config{
128 Follow: true,
129 // Use forward slashes when running a Windows binary under WSL or MSYS
130 ToSlash: fastwalk.DefaultToSlash(),
131 Sort: fastwalk.SortDirsFirst,
132 }
133 err := fastwalk.Walk(&conf, initialPath, func(path string, d os.DirEntry, err error) error {
134 if err != nil {
135 return nil // Skip files we don't have permission to access
136 }
137
138 if dl.shouldIgnore(path, ignorePatterns) {
139 if d.IsDir() {
140 return filepath.SkipDir
141 }
142 return nil
143 }
144
145 if path != initialPath {
146 if d.IsDir() {
147 path = path + string(filepath.Separator)
148 }
149 results = append(results, path)
150 }
151
152 if limit > 0 && len(results) >= limit {
153 truncated = true
154 return filepath.SkipAll
155 }
156
157 return nil
158 })
159 if err != nil && len(results) == 0 {
160 return nil, truncated, err
161 }
162
163 return results, truncated, nil
164}