package main import ( "os" "path/filepath" "sort" "testing" ) func TestInitialSync_BasicFiles(t *testing.T) { host := t.TempDir() data := t.TempDir() writeFile(t, host, "a.txt", "hello") writeFile(t, host, "b.txt", "world") writeFile(t, host, "sub/c.txt", "nested") stack := NewGitignoreStack(host) manifest, err := initialSync(host, data, stack, false) if err != nil { t.Fatal(err) } assertFileExists(t, data, "a.txt", "hello") assertFileExists(t, data, "b.txt", "world") assertFileExists(t, data, "sub/c.txt", "nested") assertManifestContains(t, manifest, "a.txt", "b.txt", "sub/c.txt") } func TestInitialSync_GitignoreExcludes(t *testing.T) { host := t.TempDir() data := t.TempDir() writeFile(t, host, ".gitignore", "*.log\nbuild/\n") writeFile(t, host, "app.go", "package main") writeFile(t, host, "debug.log", "logs here") writeFile(t, host, "build/output.o", "binary") stack := NewGitignoreStack(host) manifest, err := initialSync(host, data, stack, false) if err != nil { t.Fatal(err) } assertFileExists(t, data, "app.go", "package main") assertFileNotExists(t, data, "debug.log") assertFileNotExists(t, data, "build/output.o") assertManifestContains(t, manifest, "app.go") assertManifestNotContains(t, manifest, "debug.log", "build/output.o") } func TestInitialSync_NegationPattern(t *testing.T) { host := t.TempDir() data := t.TempDir() writeFile(t, host, ".gitignore", "*.log\n!important.log\n") writeFile(t, host, "debug.log", "debug") writeFile(t, host, "important.log", "important") stack := NewGitignoreStack(host) _, err := initialSync(host, data, stack, false) if err != nil { t.Fatal(err) } assertFileNotExists(t, data, "debug.log") assertFileExists(t, data, "important.log", "important") } func TestInitialSync_DoubleStar(t *testing.T) { host := t.TempDir() data := t.TempDir() writeFile(t, host, ".gitignore", "**/temp\n") writeFile(t, host, "src/main.go", "code") writeFile(t, host, "temp/cache", "cached") writeFile(t, host, "a/b/temp/data", "temp data") stack := NewGitignoreStack(host) _, err := initialSync(host, data, stack, false) if err != nil { t.Fatal(err) } assertFileExists(t, data, "src/main.go", "code") assertFileNotExists(t, data, "temp/cache") assertFileNotExists(t, data, "a/b/temp/data") } func TestInitialSync_DirectoryPattern(t *testing.T) { host := t.TempDir() data := t.TempDir() writeFile(t, host, ".gitignore", "node_modules/\n") writeFile(t, host, "src/app.js", "code") writeFile(t, host, "node_modules/pkg/index.js", "pkg") 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, "node_modules/pkg/index.js") } func TestInitialSync_Symlinks(t *testing.T) { host := t.TempDir() data := t.TempDir() writeFile(t, host, "target.txt", "target content") if err := os.Symlink("target.txt", filepath.Join(host, "link.txt")); err != nil { t.Fatal(err) } stack := NewGitignoreStack(host) _, err := initialSync(host, data, stack, false) if err != nil { t.Fatal(err) } linkPath := filepath.Join(data, "link.txt") info, err := os.Lstat(linkPath) if err != nil { t.Fatal(err) } if info.Mode()&os.ModeSymlink == 0 { t.Error("expected symlink, got regular file") } target, err := os.Readlink(linkPath) if err != nil { t.Fatal(err) } if target != "target.txt" { t.Errorf("symlink target = %q, want %q", target, "target.txt") } } func TestInitialSync_PreservesPermissions(t *testing.T) { host := t.TempDir() data := t.TempDir() writeFile(t, host, "script.sh", "#!/bin/sh\necho hi") os.Chmod(filepath.Join(host, "script.sh"), 0755) stack := NewGitignoreStack(host) _, err := initialSync(host, data, stack, false) if err != nil { t.Fatal(err) } info, err := os.Stat(filepath.Join(data, "script.sh")) if err != nil { t.Fatal(err) } if info.Mode().Perm() != 0755 { t.Errorf("permissions = %v, want 0755", info.Mode().Perm()) } } func TestInitialSync_DryRun(t *testing.T) { host := t.TempDir() data := t.TempDir() writeFile(t, host, "a.txt", "hello") writeFile(t, host, ".gitignore", "*.log\n") writeFile(t, host, "debug.log", "logs") stack := NewGitignoreStack(host) manifest, err := initialSync(host, data, stack, true) if err != nil { t.Fatal(err) } entries := listDir(t, data) if len(entries) != 0 { t.Errorf("dry run created files: %v", entries) } assertManifestContains(t, manifest, "a.txt") } func TestInitialSync_SkipsGit(t *testing.T) { host := t.TempDir() data := t.TempDir() writeFile(t, host, "src/main.go", "code") writeFile(t, host, ".git/config", "git config") writeFile(t, host, ".git/objects/abc", "obj") stack := NewGitignoreStack(host) _, err := initialSync(host, data, stack, false) if err != nil { t.Fatal(err) } assertFileExists(t, data, "src/main.go", "code") assertFileNotExists(t, data, ".git/config") } func TestInitialSync_NestedGitignore(t *testing.T) { host := t.TempDir() data := t.TempDir() writeFile(t, host, ".gitignore", "*.log\n") writeFile(t, host, "src/.gitignore", "*.tmp\n") writeFile(t, host, "src/app.go", "code") writeFile(t, host, "src/debug.log", "log") writeFile(t, host, "src/cache.tmp", "tmp") stack := NewGitignoreStack(host) _, err := initialSync(host, data, stack, false) if err != nil { t.Fatal(err) } assertFileExists(t, data, "src/app.go", "code") assertFileNotExists(t, data, "src/debug.log") assertFileNotExists(t, data, "src/cache.tmp") } func TestInitialSync_ComplexGitignore(t *testing.T) { host := t.TempDir() data := t.TempDir() gitignore := `# Build outputs *.o *.a build/ # Logs *.log # Temp files **/temp *.tmp # But keep important ones !important.log !lib/special.o ` writeFile(t, host, ".gitignore", gitignore) writeFile(t, host, "src/main.c", "code") writeFile(t, host, "src/main.o", "object") writeFile(t, host, "lib.a", "archive") writeFile(t, host, "build/out.o", "built") writeFile(t, host, "app.log", "log") writeFile(t, host, "important.log", "important") writeFile(t, host, "lib/special.o", "special") writeFile(t, host, "temp/cache", "cached") writeFile(t, host, "a/b/temp/data", "temp") writeFile(t, host, "file.tmp", "tmp") stack := NewGitignoreStack(host) _, err := initialSync(host, data, stack, false) if err != nil { t.Fatal(err) } assertFileExists(t, data, "src/main.c", "code") assertFileExists(t, data, "important.log", "important") assertFileExists(t, data, "lib/special.o", "special") assertFileNotExists(t, data, "src/main.o") assertFileNotExists(t, data, "lib.a") assertFileNotExists(t, data, "build/out.o") assertFileNotExists(t, data, "app.log") assertFileNotExists(t, data, "temp/cache") assertFileNotExists(t, data, "a/b/temp/data") 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) { t.Helper() full := filepath.Join(dir, path) if err := os.MkdirAll(filepath.Dir(full), 0755); err != nil { t.Fatal(err) } if err := os.WriteFile(full, []byte(content), 0644); err != nil { t.Fatal(err) } } func assertFileExists(t *testing.T, dir, path, expectedContent string) { t.Helper() full := filepath.Join(dir, path) data, err := os.ReadFile(full) if err != nil { t.Errorf("file %s does not exist: %v", path, err) return } if string(data) != expectedContent { t.Errorf("file %s content = %q, want %q", path, string(data), expectedContent) } } func assertFileNotExists(t *testing.T, dir, path string) { t.Helper() full := filepath.Join(dir, path) if _, err := os.Stat(full); err == nil { t.Errorf("file %s should not exist but does", path) } } func assertManifestContains(t *testing.T, manifest *Manifest, paths ...string) { t.Helper() for _, p := range paths { if _, ok := manifest.Files[p]; !ok { t.Errorf("manifest missing path %q", p) } } } func assertManifestNotContains(t *testing.T, manifest *Manifest, paths ...string) { t.Helper() for _, p := range paths { if _, ok := manifest.Files[p]; ok { t.Errorf("manifest should not contain path %q", p) } } } func listDir(t *testing.T, dir string) []string { t.Helper() entries, err := os.ReadDir(dir) if err != nil { t.Fatal(err) } var names []string for _, e := range entries { names = append(names, e.Name()) } sort.Strings(names) return names }