aboutsummaryrefslogtreecommitdiffstats
path: root/sync_test.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 /sync_test.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 'sync_test.go')
-rw-r--r--sync_test.go197
1 files changed, 137 insertions, 60 deletions
diff --git a/sync_test.go b/sync_test.go
index 758d4ec..4827a49 100644
--- a/sync_test.go
+++ b/sync_test.go
@@ -5,8 +5,6 @@ import (
"path/filepath"
"sort"
"testing"
-
- ignore "github.com/sabhiram/go-gitignore"
)
func TestInitialSync_BasicFiles(t *testing.T) {
@@ -17,9 +15,9 @@ func TestInitialSync_BasicFiles(t *testing.T) {
writeFile(t, host, "b.txt", "world")
writeFile(t, host, "sub/c.txt", "nested")
- rules := ignore.CompileIgnoreLines()
+ stack := NewGitignoreStack(host)
- manifest, err := initialSync(host, data, rules, false)
+ manifest, err := initialSync(host, data, stack, false)
if err != nil {
t.Fatal(err)
}
@@ -39,12 +37,9 @@ func TestInitialSync_GitignoreExcludes(t *testing.T) {
writeFile(t, host, "debug.log", "logs here")
writeFile(t, host, "build/output.o", "binary")
- rules, err := loadGitignore(host)
- if err != nil {
- t.Fatal(err)
- }
+ stack := NewGitignoreStack(host)
- manifest, err := initialSync(host, data, rules, false)
+ manifest, err := initialSync(host, data, stack, false)
if err != nil {
t.Fatal(err)
}
@@ -64,12 +59,9 @@ func TestInitialSync_NegationPattern(t *testing.T) {
writeFile(t, host, "debug.log", "debug")
writeFile(t, host, "important.log", "important")
- rules, err := loadGitignore(host)
- if err != nil {
- t.Fatal(err)
- }
+ stack := NewGitignoreStack(host)
- _, err = initialSync(host, data, rules, false)
+ _, err := initialSync(host, data, stack, false)
if err != nil {
t.Fatal(err)
}
@@ -87,12 +79,9 @@ func TestInitialSync_DoubleStar(t *testing.T) {
writeFile(t, host, "temp/cache", "cached")
writeFile(t, host, "a/b/temp/data", "temp data")
- rules, err := loadGitignore(host)
- if err != nil {
- t.Fatal(err)
- }
+ stack := NewGitignoreStack(host)
- _, err = initialSync(host, data, rules, false)
+ _, err := initialSync(host, data, stack, false)
if err != nil {
t.Fatal(err)
}
@@ -110,12 +99,9 @@ func TestInitialSync_DirectoryPattern(t *testing.T) {
writeFile(t, host, "src/app.js", "code")
writeFile(t, host, "node_modules/pkg/index.js", "pkg")
- rules, err := loadGitignore(host)
- if err != nil {
- t.Fatal(err)
- }
+ stack := NewGitignoreStack(host)
- _, err = initialSync(host, data, rules, false)
+ _, err := initialSync(host, data, stack, false)
if err != nil {
t.Fatal(err)
}
@@ -133,9 +119,9 @@ func TestInitialSync_Symlinks(t *testing.T) {
t.Fatal(err)
}
- rules := ignore.CompileIgnoreLines()
+ stack := NewGitignoreStack(host)
- _, err := initialSync(host, data, rules, false)
+ _, err := initialSync(host, data, stack, false)
if err != nil {
t.Fatal(err)
}
@@ -167,9 +153,9 @@ func TestInitialSync_PreservesPermissions(t *testing.T) {
writeFile(t, host, "script.sh", "#!/bin/sh\necho hi")
os.Chmod(filepath.Join(host, "script.sh"), 0755)
- rules := ignore.CompileIgnoreLines()
+ stack := NewGitignoreStack(host)
- _, err := initialSync(host, data, rules, false)
+ _, err := initialSync(host, data, stack, false)
if err != nil {
t.Fatal(err)
}
@@ -192,23 +178,18 @@ func TestInitialSync_DryRun(t *testing.T) {
writeFile(t, host, ".gitignore", "*.log\n")
writeFile(t, host, "debug.log", "logs")
- rules, err := loadGitignore(host)
- if err != nil {
- t.Fatal(err)
- }
+ stack := NewGitignoreStack(host)
- manifest, err := initialSync(host, data, rules, true)
+ manifest, err := initialSync(host, data, stack, true)
if err != nil {
t.Fatal(err)
}
- // Dry run should not create any files
entries := listDir(t, data)
if len(entries) != 0 {
t.Errorf("dry run created files: %v", entries)
}
- // But manifest should be populated
assertManifestContains(t, manifest, "a.txt")
}
@@ -220,9 +201,9 @@ func TestInitialSync_SkipsGit(t *testing.T) {
writeFile(t, host, ".git/config", "git config")
writeFile(t, host, ".git/objects/abc", "obj")
- rules := ignore.CompileIgnoreLines()
+ stack := NewGitignoreStack(host)
- _, err := initialSync(host, data, rules, false)
+ _, err := initialSync(host, data, stack, false)
if err != nil {
t.Fatal(err)
}
@@ -241,25 +222,9 @@ func TestInitialSync_NestedGitignore(t *testing.T) {
writeFile(t, host, "src/debug.log", "log")
writeFile(t, host, "src/cache.tmp", "tmp")
- // Load root gitignore
- rules, err := loadGitignore(host)
- if err != nil {
- t.Fatal(err)
- }
+ stack := NewGitignoreStack(host)
- // Load nested gitignore and add its rules
- nestedData, err := os.ReadFile(filepath.Join(host, "src", ".gitignore"))
- if err == nil {
- _ = nestedData // loaded for verification
- // Merge: check both root and nested
- combined := ignore.CompileIgnoreLines(
- "*.log",
- "*.tmp",
- )
- rules = combined
- }
-
- _, err = initialSync(host, data, rules, false)
+ _, err := initialSync(host, data, stack, false)
if err != nil {
t.Fatal(err)
}
@@ -301,12 +266,9 @@ build/
writeFile(t, host, "a/b/temp/data", "temp")
writeFile(t, host, "file.tmp", "tmp")
- rules, err := loadGitignore(host)
- if err != nil {
- t.Fatal(err)
- }
+ stack := NewGitignoreStack(host)
- _, err = initialSync(host, data, rules, false)
+ _, err := initialSync(host, data, stack, false)
if err != nil {
t.Fatal(err)
}
@@ -324,6 +286,121 @@ build/
assertFileNotExists(t, data, "file.tmp")
}
+func TestInitialSync_NestedGitignore_Scoped(t *testing.T) {
+ host := t.TempDir()
+ data := t.TempDir()
+
+ writeFile(t, host, ".gitignore", "")
+ writeFile(t, host, "a/.gitignore", "*.log\n")
+ writeFile(t, host, "b/.gitignore", "*.tmp\n")
+
+ writeFile(t, host, "a/debug.log", "log")
+ writeFile(t, host, "a/file.go", "code")
+ writeFile(t, host, "b/cache.tmp", "tmp")
+ writeFile(t, host, "b/file.go", "code")
+ writeFile(t, host, "c/debug.log", "log")
+
+ stack := NewGitignoreStack(host)
+
+ _, err := initialSync(host, data, stack, false)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ assertFileNotExists(t, data, "a/debug.log")
+ assertFileExists(t, data, "a/file.go", "code")
+ assertFileNotExists(t, data, "b/cache.tmp")
+ assertFileExists(t, data, "b/file.go", "code")
+ assertFileExists(t, data, "c/debug.log", "log")
+}
+
+func TestInitialSync_NestedGitignore_DirScoped(t *testing.T) {
+ host := t.TempDir()
+ data := t.TempDir()
+
+ writeFile(t, host, ".gitignore", "build/\n")
+ writeFile(t, host, "src/.gitignore", "dist/\n")
+ writeFile(t, host, "src/app.js", "code")
+ writeFile(t, host, "src/dist/bundle.js", "bundle")
+ writeFile(t, host, "build/output.o", "built")
+
+ stack := NewGitignoreStack(host)
+
+ _, err := initialSync(host, data, stack, false)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ assertFileExists(t, data, "src/app.js", "code")
+ assertFileNotExists(t, data, "src/dist/bundle.js")
+ assertFileNotExists(t, data, "build/output.o")
+}
+
+func TestInitialSync_NestedGitignore_NegationOverride(t *testing.T) {
+ host := t.TempDir()
+ data := t.TempDir()
+
+ writeFile(t, host, ".gitignore", "*.txt\n")
+ writeFile(t, host, "sub/.gitignore", "!important.txt\n")
+ writeFile(t, host, "sub/readme.txt", "readme")
+ writeFile(t, host, "sub/important.txt", "important")
+
+ stack := NewGitignoreStack(host)
+
+ _, err := initialSync(host, data, stack, false)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ assertFileNotExists(t, data, "sub/readme.txt")
+ assertFileExists(t, data, "sub/important.txt", "important")
+}
+
+func TestInitialSync_NestedGitignore_DeepNesting(t *testing.T) {
+ host := t.TempDir()
+ data := t.TempDir()
+
+ writeFile(t, host, ".gitignore", "*.log\n")
+ writeFile(t, host, "a/.gitignore", "*.tmp\n")
+ writeFile(t, host, "a/b/.gitignore", "*.bak\n")
+
+ writeFile(t, host, "a/b/c/debug.log", "log")
+ writeFile(t, host, "a/b/c/cache.tmp", "tmp")
+ writeFile(t, host, "a/b/c/backup.bak", "bak")
+ writeFile(t, host, "a/b/c/file.go", "code")
+
+ stack := NewGitignoreStack(host)
+
+ _, err := initialSync(host, data, stack, false)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ assertFileNotExists(t, data, "a/b/c/debug.log")
+ assertFileNotExists(t, data, "a/b/c/cache.tmp")
+ assertFileNotExists(t, data, "a/b/c/backup.bak")
+ assertFileExists(t, data, "a/b/c/file.go", "code")
+}
+
+func TestInitialSync_NestedGitignore_ParentScopeDoesntLeak(t *testing.T) {
+ host := t.TempDir()
+ data := t.TempDir()
+
+ writeFile(t, host, "parent/.gitignore", "*.log\n")
+ writeFile(t, host, "sibling/file.go", "code")
+ writeFile(t, host, "sibling/debug.log", "log")
+
+ stack := NewGitignoreStack(host)
+
+ _, err := initialSync(host, data, stack, false)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ assertFileExists(t, data, "sibling/file.go", "code")
+ assertFileExists(t, data, "sibling/debug.log", "log")
+}
+
// Helper functions
func writeFile(t *testing.T, dir, path, content string) {