12 "github.com/charlievieth/fastwalk"
13 "github.com/charmbracelet/crush/internal/csync"
14 "github.com/charmbracelet/crush/internal/home"
15 ignore "github.com/sabhiram/go-gitignore"
18// commonIgnorePatterns contains commonly ignored files and directories
19var commonIgnorePatterns = sync.OnceValue(func() ignore.IgnoreParser {
20 return ignore.CompileIgnoreLines(
27 // IDE and editor files
36 // Build artifacts and dependencies
50 // Logs and temporary files
83var homeIgnore = sync.OnceValue(func() ignore.IgnoreParser {
86 for _, name := range []string{
87 filepath.Join(home, ".gitignore"),
88 filepath.Join(home, ".config", "git", "ignore"),
89 filepath.Join(home, ".config", "crush", "ignore"),
91 if bts, err := os.ReadFile(name); err == nil {
92 lines = append(lines, strings.Split(string(bts), "\n")...)
95 return ignore.CompileIgnoreLines(lines...)
98type directoryLister struct {
99 ignores *csync.Map[string, ignore.IgnoreParser]
103func NewDirectoryLister(rootPath string) *directoryLister {
104 dl := &directoryLister{
106 ignores: csync.NewMap[string, ignore.IgnoreParser](),
108 dl.getIgnore(rootPath)
112// git checks, in order:
113// - ./.gitignore, ../.gitignore, etc, until repo root
114// ~/.config/git/ignore
117// This will do the following:
118// - the given ignorePatterns
119// - [commonIgnorePatterns]
120// - ./.gitignore, ../.gitignore, etc, until dl.rootPath
121// - ./.crushignore, ../.crushignore, etc, until dl.rootPath
122// ~/.config/git/ignore
124// ~/.config/crush/ignore
125func (dl *directoryLister) shouldIgnore(path string, ignorePatterns []string) bool {
126 if len(ignorePatterns) > 0 {
127 base := filepath.Base(path)
128 for _, pattern := range ignorePatterns {
129 if matched, err := filepath.Match(pattern, base); err == nil && matched {
135 // Don't apply gitignore rules to the root directory itself
136 // In gitignore semantics, patterns don't apply to the repo root
137 if path == dl.rootPath {
141 relPath, err := filepath.Rel(dl.rootPath, path)
146 if commonIgnorePatterns().MatchesPath(relPath) {
147 slog.Debug("ignoring common pattern", "path", relPath)
151 parentDir := filepath.Dir(path)
152 ignoreParser := dl.getIgnore(parentDir)
153 if ignoreParser.MatchesPath(relPath) {
154 slog.Debug("ignoring dir pattern", "path", relPath, "dir", parentDir)
158 // For directories, also check with trailing slash (gitignore convention)
159 if ignoreParser.MatchesPath(relPath + "/") {
160 slog.Debug("ignoring dir pattern with slash", "path", relPath+"/", "dir", parentDir)
164 if dl.checkParentIgnores(relPath) {
168 if homeIgnore().MatchesPath(relPath) {
169 slog.Debug("ignoring home dir pattern", "path", relPath)
176func (dl *directoryLister) checkParentIgnores(path string) bool {
177 parent := filepath.Dir(filepath.Dir(path))
178 for parent != "." && path != "." {
179 if dl.getIgnore(parent).MatchesPath(path) {
180 slog.Debug("ingoring parent dir pattern", "path", path, "dir", parent)
183 if parent == dl.rootPath {
186 parent = filepath.Dir(parent)
191func (dl *directoryLister) getIgnore(path string) ignore.IgnoreParser {
192 return dl.ignores.GetOrSet(path, func() ignore.IgnoreParser {
194 for _, ign := range []string{".crushignore", ".gitignore"} {
195 name := filepath.Join(path, ign)
196 if content, err := os.ReadFile(name); err == nil {
197 lines = append(lines, strings.Split(string(content), "\n")...)
201 // Return a no-op parser to avoid nil checks
202 return ignore.CompileIgnoreLines()
204 return ignore.CompileIgnoreLines(lines...)
208// ListDirectory lists files and directories in the specified path,
209func ListDirectory(initialPath string, ignorePatterns []string, depth, limit int) ([]string, bool, error) {
210 found := csync.NewSlice[string]()
211 dl := NewDirectoryLister(initialPath)
213 slog.Debug("listing directory", "path", initialPath, "depth", depth, "limit", limit, "ignorePatterns", ignorePatterns)
215 conf := fastwalk.Config{
217 ToSlash: fastwalk.DefaultToSlash(),
218 Sort: fastwalk.SortDirsFirst,
222 err := fastwalk.Walk(&conf, initialPath, func(path string, d os.DirEntry, err error) error {
224 return nil // Skip files we don't have permission to access
227 if dl.shouldIgnore(path, ignorePatterns) {
229 return filepath.SkipDir
234 if path != initialPath {
236 path = path + string(filepath.Separator)
241 if limit > 0 && found.Len() >= limit {
242 return filepath.SkipAll
247 if err != nil && !errors.Is(err, filepath.SkipAll) {
248 return nil, false, err
251 matches, truncated := truncate(slices.Collect(found.Seq()), limit)
252 return matches, truncated || errors.Is(err, filepath.SkipAll), nil
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list