ls.go

  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}