diff --git a/internal/digest/ingest.go b/internal/digest/ingest.go index 138fa9d..b05e572 100644 --- a/internal/digest/ingest.go +++ b/internal/digest/ingest.go @@ -343,10 +343,21 @@ func isPathMatchWithInfo(relativePath string, isDir bool, patterns []string) boo pathToCheckPrefix = parentDir + "/" } + // Check if path starts with the pattern (e.g. ".git/HEAD" matches ".git/") if strings.HasPrefix(pathToCheckPrefix, cleanPattern+"/") { return true } + // Check if the pattern matches any segment of the path. + // This handles nested directories like "vendor/.git/" matching ".git/". + if !strings.Contains(cleanPattern, "/") { + // Simple dir name pattern (e.g. ".git/", "node_modules/") + // Check if any path segment matches + if strings.Contains("/"+pathToCheckPrefix, "/"+cleanPattern+"/") { + return true + } + } + if isDir && strings.HasPrefix(cleanPattern+"/", pathToCheckPrefix) { return true } diff --git a/internal/digest/ingest_test.go b/internal/digest/ingest_test.go index ed490c2..91fde36 100644 --- a/internal/digest/ingest_test.go +++ b/internal/digest/ingest_test.go @@ -43,6 +43,14 @@ func TestIsPathMatchWithInfo(t *testing.T) { {"Pattern of dir vs file with same name", "data", false, []string{"data/"}, false}, {"Pattern of file vs dir with same name", "data/", true, []string{"data"}, true}, + // Nested directory exclusion patterns + {"Nested .git dir", "vendor/lib/.git", true, []string{".git/"}, true}, + {"Nested .git file", "vendor/lib/.git/HEAD", false, []string{".git/"}, true}, + {"Nested .git deep", "a/b/c/.git/objects/pack", false, []string{".git/"}, true}, + {"Nested node_modules", "packages/app/node_modules", true, []string{"node_modules/"}, true}, + {"Nested node_modules file", "packages/app/node_modules/pkg/index.js", false, []string{"node_modules/"}, true}, + {"Nested .next dir", "apps/web/.next", true, []string{".next/"}, true}, + {"Nested build dir", "packages/lib/build", true, []string{"build/"}, true}, {"No patterns", "file.txt", false, []string{}, false}, {"Empty path, no patterns", "", false, []string{}, false}, {"Empty path, with pattern", "", false, []string{"*.txt"}, false}, diff --git a/internal/fsutil/fs.go b/internal/fsutil/fs.go index 1c9f43d..0ebf7aa 100644 --- a/internal/fsutil/fs.go +++ b/internal/fsutil/fs.go @@ -55,7 +55,13 @@ func GetRelativePath(basePath, targetPath string) (string, error) { return "", err } - if !strings.HasPrefix(absTargetPath, absBasePath) { + // Ensure we check at a path segment boundary to avoid + // false matches like "/tmp" matching "/tmp2" + baseWithSep := absBasePath + if !strings.HasSuffix(baseWithSep, string(filepath.Separator)) { + baseWithSep += string(filepath.Separator) + } + if absTargetPath != absBasePath && !strings.HasPrefix(absTargetPath, baseWithSep) { return filepath.Base(absTargetPath), nil } diff --git a/internal/fsutil/fs_test.go b/internal/fsutil/fs_test.go new file mode 100644 index 0000000..5fd9652 --- /dev/null +++ b/internal/fsutil/fs_test.go @@ -0,0 +1,66 @@ +package fsutil + +import ( + "os" + "path/filepath" + "testing" +) + +func TestGetRelativePath(t *testing.T) { + // Create temp dirs for realistic path testing + tmpDir := t.TempDir() + baseDir := filepath.Join(tmpDir, "project") + nestedDir := filepath.Join(baseDir, "src", "lib") + similarDir := filepath.Join(tmpDir, "project2", "file.txt") + + if err := os.MkdirAll(nestedDir, 0755); err != nil { + t.Fatalf("failed to create nested test directory %q: %v", nestedDir, err) + } + if err := os.MkdirAll(filepath.Dir(similarDir), 0755); err != nil { + t.Fatalf("failed to create similar test directory %q: %v", filepath.Dir(similarDir), err) + } + if err := os.WriteFile(filepath.Join(nestedDir, "main.go"), []byte("package main"), 0644); err != nil { + t.Fatalf("failed to create nested test file %q: %v", filepath.Join(nestedDir, "main.go"), err) + } + if err := os.WriteFile(similarDir, []byte("hello"), 0644); err != nil { + t.Fatalf("failed to create similar test file %q: %v", similarDir, err) + } + + tests := []struct { + name string + base string + target string + expected string + }{ + { + name: "nested file", + base: baseDir, + target: filepath.Join(nestedDir, "main.go"), + expected: filepath.Join("src", "lib", "main.go"), + }, + { + name: "same directory", + base: baseDir, + target: baseDir, + expected: ".", + }, + { + name: "similar prefix should not match", + base: baseDir, + target: similarDir, + expected: "file.txt", // Should fall back to basename, not "2/file.txt" + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := GetRelativePath(tt.base, tt.target) + if err != nil { + t.Fatalf("GetRelativePath(%q, %q) returned error: %v", tt.base, tt.target, err) + } + if got != tt.expected { + t.Errorf("GetRelativePath(%q, %q) = %q, want %q", tt.base, tt.target, got, tt.expected) + } + }) + } +}