diff --git a/internal/snapshot/exclude_test.go b/internal/snapshot/exclude_test.go index 9870018..48deb70 100644 --- a/internal/snapshot/exclude_test.go +++ b/internal/snapshot/exclude_test.go @@ -531,3 +531,50 @@ func TestExcludePatterns_UnanchoredPatternFile(t *testing.T) { require.Equal(t, 1, result.FilesScanned, "Unanchored pattern for file should match anywhere") } + +func TestExcludePatterns_DoubleStarDirectoryAtEveryDepth(t *testing.T) { + t.Parallel() + + // As in gitignore, a leading **/ also matches directly under the source dir + fs := setupAnchoredTestFS(t) + + scanner, repos, cleanup := createTestScanner(t, fs, []string{"**/projectname"}) + defer cleanup() + + ctx := context.Background() + createSnapshotRecord(ctx, t, repos, "test-snapshot") + + result, err := scanner.Scan(ctx, "/backup", "test-snapshot") + require.NoError(t, err) + + // **/projectname should exclude BOTH: + // - /backup/projectname/file.txt + // - /backup/otherproject/projectname/file.txt + // Total files: 4, excluded: 2 + require.Equal(t, 2, result.FilesScanned, + "**/ directory pattern should match at the top level and nested") +} + +func TestExcludePatterns_DoubleStarFileAtEveryDepth(t *testing.T) { + t.Parallel() + + // As in gitignore, a leading **/ also matches directly under the source dir + fs := setupAnchoredTestFS(t) + + scanner, repos, cleanup := createTestScanner(t, fs, []string{"**/*.txt"}) + defer cleanup() + + ctx := context.Background() + createSnapshotRecord(ctx, t, repos, "test-snapshot") + + result, err := scanner.Scan(ctx, "/backup", "test-snapshot") + require.NoError(t, err) + + // **/*.txt should exclude ALL .txt files: + // - /backup/file.txt + // - /backup/projectname/file.txt + // - /backup/otherproject/projectname/file.txt + // Total files: 4, excluded: 3 + require.Equal(t, 1, result.FilesScanned, + "**/ file pattern should match at the top level and nested") +} diff --git a/internal/snapshot/scanner.go b/internal/snapshot/scanner.go index dc98d4f..e54ee1a 100644 --- a/internal/snapshot/scanner.go +++ b/internal/snapshot/scanner.go @@ -2018,6 +2018,14 @@ func compileExcludePatterns(patterns []string) []compiledPattern { // Remove trailing slash if present (directory indicator) pattern = strings.TrimSuffix(pattern, "/") + // A leading "**/" demands a "/" before the name and so misses an + // entry directly under the source path. An unanchored pattern + // already matches at every depth, so drop the prefix: as in + // gitignore, "**/x" then also matches a top-level "x". + if !anchored { + pattern = strings.TrimPrefix(pattern, "**/") + } + // Compile the glob pattern // For patterns without path separators, we need to match them as components // e.g., ".git" should match ".git" anywhere in the path