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