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	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}