aboutsummaryrefslogtreecommitdiffstats
path: root/gitignore.go
diff options
context:
space:
mode:
authorBernhard Guillon <Bernhard.Guillon@begu.org>2026-07-06 21:36:29 +0200
committerBernhard Guillon <Bernhard.Guillon@begu.org>2026-07-06 21:36:29 +0200
commitad7575e85ebf6332d324ca5997db18ea81065df4 (patch)
treed2a2e46e2c0432d4b4437c562b4c70de4a6bebf0 /gitignore.go
parent8b22a36d1c068f9baee68799b0c1f51ecb999e9d (diff)
downloadsourcewatch-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.go119
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))
}