ls.go

  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}