diff options
| author | Bernhard Guillon <Bernhard.Guillon@begu.org> | 2026-07-06 21:36:29 +0200 |
|---|---|---|
| committer | Bernhard Guillon <Bernhard.Guillon@begu.org> | 2026-07-06 21:36:29 +0200 |
| commit | ad7575e85ebf6332d324ca5997db18ea81065df4 (patch) | |
| tree | d2a2e46e2c0432d4b4437c562b4c70de4a6bebf0 /gitignore.go | |
| parent | 8b22a36d1c068f9baee68799b0c1f51ecb999e9d (diff) | |
| download | sourcewatch-ad7575e85ebf6332d324ca5997db18ea81065df4.tar.gz sourcewatch-ad7575e85ebf6332d324ca5997db18ea81065df4.zip | |
Implement nested .gitignore support with GitignoreStack
- Replaced go-gitignore with custom GitignoreStack that tracks
multiple .gitignore files hierarchically
- Patterns are processed in root-to-leaf order, last match wins
- Negation patterns in child dirs override parent ignores
- Patterns only apply within their directory scope (no leaking)
- Added 6 new tests for nested .gitignore:
- Scoped: patterns only affect own directory
- Dir-scoped: different dirs can ignore different things
- Negation override: child !pattern un-ignores parent's *.pattern
- Deep nesting: 3+ levels of .gitignore
- Parent scope doesn't leak to siblings
- All 35 tests passing
Diffstat (limited to 'gitignore.go')
| -rw-r--r-- | gitignore.go | 119 |
1 files changed, 115 insertions, 4 deletions
diff --git a/gitignore.go b/gitignore.go index 80d14a6..569e191 100644 --- a/gitignore.go +++ b/gitignore.go @@ -1,18 +1,129 @@ package main import ( + "bufio" "os" "path/filepath" + "strings" ignore "github.com/sabhiram/go-gitignore" ) -func loadGitignore(hostDir string) (*ignore.GitIgnore, error) { - gitignorePath := filepath.Join(hostDir, ".gitignore") +type gitignoreLayer struct { + dir string + posRules *ignore.GitIgnore + negRules *ignore.GitIgnore +} + +type GitignoreStack struct { + rootDir string + layers []gitignoreLayer +} + +func NewGitignoreStack(rootDir string) *GitignoreStack { + stack := &GitignoreStack{rootDir: rootDir} + stack.loadAndPush(rootDir) + return stack +} + +func (s *GitignoreStack) Push(dir string) { + s.loadAndPush(dir) +} + +func (s *GitignoreStack) Pop() { + if len(s.layers) > 0 { + s.layers = s.layers[:len(s.layers)-1] + } +} + +func (s *GitignoreStack) IsIgnored(relPath string) bool { + absPath := filepath.Join(s.rootDir, relPath) + result := false + + for _, layer := range s.layers { + childRel, err := filepath.Rel(layer.dir, absPath) + if err != nil { + continue + } + childRel = filepath.ToSlash(childRel) + if strings.HasPrefix(childRel, "../") { + continue + } + + posMatch := layer.posRules != nil && layer.posRules.MatchesPath(childRel) + negMatch := layer.negRules != nil && layer.negRules.MatchesPath(childRel) + + if negMatch { + result = false + } else if posMatch { + result = true + } + } + return result +} +func (s *GitignoreStack) ParentIgnored(dir string) bool { + parent := filepath.Dir(dir) + if parent == dir { + return false + } + parentRel, err := filepath.Rel(s.rootDir, parent) + if err != nil { + return false + } + if parentRel == "." { + return false + } + return s.IsIgnored(parentRel) +} + +func (s *GitignoreStack) loadAndPush(dir string) { + gitignorePath := filepath.Join(dir, ".gitignore") if _, err := os.Stat(gitignorePath); os.IsNotExist(err) { - return ignore.CompileIgnoreLines(), nil + return + } + + f, err := os.Open(gitignorePath) + if err != nil { + return + } + defer f.Close() + + var posLines, negLines []string + scanner := bufio.NewScanner(f) + for scanner.Scan() { + line := scanner.Text() + trimmed := strings.TrimSpace(line) + if trimmed == "" || strings.HasPrefix(trimmed, "#") { + continue + } + if strings.HasPrefix(trimmed, "!") { + negLines = append(negLines, trimmed[1:]) + } else { + posLines = append(posLines, trimmed) + } + } + + var posRules, negRules *ignore.GitIgnore + if len(posLines) > 0 { + posRules = ignore.CompileIgnoreLines(posLines...) + } + if len(negLines) > 0 { + negRules = ignore.CompileIgnoreLines(negLines...) } - return ignore.CompileIgnoreFile(gitignorePath) + s.layers = append(s.layers, gitignoreLayer{ + dir: dir, + posRules: posRules, + negRules: negRules, + }) +} + +func hasGitignore(dir string) bool { + _, err := os.Stat(filepath.Join(dir, ".gitignore")) + return err == nil +} + +func isGitDir(path string) bool { + return path == ".git" || strings.HasPrefix(path, ".git"+string(filepath.Separator)) } |
