Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions internal/digest/ingest.go
Original file line number Diff line number Diff line change
Expand Up @@ -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/")

Copilot AI Apr 13, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment is slightly misleading: for files, the code checks the parent directory prefix (pathToCheckPrefix), not the full file path. Consider rewording the example to reflect what is actually being compared (e.g., that a file under ".git/" has parentDir ".git/" which matches the pattern).

Suggested change
// Check if path starts with the pattern (e.g. ".git/HEAD" matches ".git/")
// Check whether the directory prefix being examined matches the pattern.
// For directories this is the directory path itself; for files it is the
// parent directory (e.g. ".git/HEAD" has parentDir ".git/", which matches ".git/").

Copilot uses AI. Check for mistakes.
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
}
Expand Down
8 changes: 8 additions & 0 deletions internal/digest/ingest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down
8 changes: 7 additions & 1 deletion internal/fsutil/fs.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
66 changes: 66 additions & 0 deletions internal/fsutil/fs_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}