diff --git a/pkg/fsx/vcs.go b/pkg/fsx/vcs.go index 8e9053e767..61b59bb761 100644 --- a/pkg/fsx/vcs.go +++ b/pkg/fsx/vcs.go @@ -219,16 +219,17 @@ func (m *VCSMatcher) ShouldIgnore(path string) bool { absPath = resolved } - // Check if path is within this repository - if !strings.HasPrefix(absPath, m.repoRoot) { - return false - } - - // Create a relative path from the repository root for matching + // Create a relative path from the repository root for matching. Rel doubles + // as the containment check: a path outside the repository climbs out with + // "..", which a string-prefix test would miss for a sibling directory whose + // name merely starts with the root's ("-sibling"). relPath, err := filepath.Rel(m.repoRoot, absPath) if err != nil { return false } + if relPath == ".." || strings.HasPrefix(relPath, ".."+string(filepath.Separator)) { + return false + } // Check if the path is a directory info, err := os.Stat(path) diff --git a/pkg/fsx/vcs_test.go b/pkg/fsx/vcs_test.go index ac4f9be5d9..a4f0d449a3 100644 --- a/pkg/fsx/vcs_test.go +++ b/pkg/fsx/vcs_test.go @@ -158,4 +158,52 @@ func TestNewVCSMatcher(t *testing.T) { assert.True(t, m.ShouldIgnore(filepath.Join(sub, "debug.log"))) }) + + // A path outside the repository must never be matched against the + // repository's patterns. A plain string-prefix containment test has no + // path-component boundary, so "-sibling" looks like it lives under + // "" and filepath.Rel yields "../-sibling/...", whose trailing + // component then matches a basename pattern such as "*.log". + t.Run("sibling directory sharing the root name prefix is outside the repository", func(t *testing.T) { + t.Parallel() + parent := t.TempDir() + + repo := filepath.Join(parent, "repo") + require.NoError(t, os.Mkdir(repo, 0o755)) + writeGitDir(t, repo) + require.NoError(t, os.WriteFile(filepath.Join(repo, ".gitignore"), []byte("*.log\n"), 0o644)) + require.NoError(t, os.WriteFile(filepath.Join(repo, "app.log"), nil, 0o644)) + + sibling := filepath.Join(parent, "repo-sibling") + require.NoError(t, os.Mkdir(sibling, 0o755)) + require.NoError(t, os.WriteFile(filepath.Join(sibling, "app.log"), nil, 0o644)) + + unrelated := filepath.Join(parent, "other") + require.NoError(t, os.Mkdir(unrelated, 0o755)) + require.NoError(t, os.WriteFile(filepath.Join(unrelated, "app.log"), nil, 0o644)) + + m, err := NewVCSMatcher(repo) + require.NoError(t, err) + require.NotNil(t, m) + + assert.True(t, m.ShouldIgnore(filepath.Join(repo, "app.log")), + "a matching file inside the repository is still ignored") + assert.False(t, m.ShouldIgnore(filepath.Join(sibling, "app.log")), + "a file in a sibling directory is outside the repository") + assert.False(t, m.ShouldIgnore(filepath.Join(unrelated, "app.log")), + "a file in an unrelated directory is outside the repository") + }) + + t.Run("repository root itself is matched against its own patterns", func(t *testing.T) { + t.Parallel() + dir := t.TempDir() + writeGitDir(t, dir) + require.NoError(t, os.WriteFile(filepath.Join(dir, ".gitignore"), []byte("*.log\n"), 0o644)) + + m, err := NewVCSMatcher(dir) + require.NoError(t, err) + require.NotNil(t, m) + + assert.False(t, m.ShouldIgnore(dir), "the root is not itself ignored") + }) }