Skip to content
Open
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
37 changes: 37 additions & 0 deletions internal/projectpath/path.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,43 @@ func CheckedRuntimeCodemapDir(projectRoot string) (string, error) {
return filepath.Join(selection.RuntimeDir, "projects", ProjectKey(selection.ProjectRoot)), nil
}

// CanonicalPath returns an absolute, cleaned path with platform symlinks
// resolved; missing trailing components are retained for event paths.
func CanonicalPath(path string) string {
absPath, err := filepath.Abs(path)
if err != nil {
return filepath.Clean(path)
}
if canonical, err := filepath.EvalSymlinks(absPath); err == nil {
return filepath.Clean(canonical)
} else if !os.IsNotExist(err) {
return filepath.Clean(absPath)
}

current := absPath
var suffix []string
for {
parent := filepath.Dir(current)
if parent == current {
break
}
suffix = append(suffix, filepath.Base(current))
current = parent
canonical, err := filepath.EvalSymlinks(current)
if err != nil {
if !os.IsNotExist(err) {
return filepath.Clean(absPath)
}
continue
}
for i := len(suffix) - 1; i >= 0; i-- {
canonical = filepath.Join(canonical, suffix[i])
}
return filepath.Clean(canonical)
}
return filepath.Clean(absPath)
}

func canonicalProjectRoot(root string) (string, error) {
absRoot, err := filepath.Abs(root)
if err != nil {
Expand Down
28 changes: 28 additions & 0 deletions internal/projectpath/path_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,34 @@ func TestRuntimeRootAndCheckedRuntimeCodemapDir(t *testing.T) {
}
}

func TestCanonicalPathResolvesAliasesWithMissingLeaf(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("symlinks may require elevated privileges")
}
target := t.TempDir()
if err := os.WriteFile(filepath.Join(target, "existing.txt"), nil, 0o644); err != nil {
t.Fatal(err)
}
alias := filepath.Join(t.TempDir(), "alias")
if err := os.Symlink(target, alias); err != nil {
t.Fatal(err)
}
canonicalTarget, err := filepath.EvalSymlinks(target)
if err != nil {
t.Fatal(err)
}

tests := map[string]string{
filepath.Join(alias, "existing.txt"): filepath.Join(canonicalTarget, "existing.txt"),
filepath.Join(alias, "missing", "config.json"): filepath.Join(canonicalTarget, "missing", "config.json"),
}
for path, want := range tests {
if got := CanonicalPath(path); got != want {
t.Fatalf("CanonicalPath(%q) = %q, want %q", path, got, want)
}
}
}

func TestProjectKeyScopesProjectsAndSharesRepoKey(t *testing.T) {
a := filepath.Join(t.TempDir(), "projA")
if err := os.MkdirAll(filepath.Join(a, ".git"), 0o755); err != nil {
Expand Down
10 changes: 6 additions & 4 deletions scanner/walker.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"strings"

"codemap/config"
"codemap/internal/projectpath"

ignore "github.com/sabhiram/go-gitignore"
)
Expand All @@ -25,7 +26,7 @@ type GitIgnoreCache struct {
// NewGitIgnoreCache creates a cache that supports nested .gitignore files.
// root should be the project root directory.
func NewGitIgnoreCache(root string) *GitIgnoreCache {
absRoot, _ := filepath.Abs(root)
absRoot := projectpath.CanonicalPath(root)
c := &GitIgnoreCache{
root: absRoot,
cache: make(map[string]*ignore.GitIgnore),
Expand Down Expand Up @@ -72,7 +73,7 @@ func (c *GitIgnoreCache) EnsureDir(dir string) {
if c == nil || dir == "" {
return
}
c.tryLoadGitignore(dir)
c.tryLoadGitignore(projectpath.CanonicalPath(dir))
}

// ShouldIgnore checks if a path should be ignored based on all applicable .gitignore files.
Expand All @@ -81,6 +82,7 @@ func (c *GitIgnoreCache) ShouldIgnore(absPath string) bool {
if len(c.cache) == 0 {
return false
}
absPath = projectpath.CanonicalPath(absPath)

// Collect directories from leaf to root
var dirs []string
Expand Down Expand Up @@ -233,9 +235,9 @@ func ScanFiles(ctx context.Context, root string, cache *GitIgnoreCache, only []s
return nil, err
}
var files []FileInfo
absRoot, _ := filepath.Abs(root)
absRoot := projectpath.CanonicalPath(root)

err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
err := filepath.Walk(absRoot, func(path string, info os.FileInfo, err error) error {
if ctxErr := ctx.Err(); ctxErr != nil {
return ctxErr
}
Expand Down
11 changes: 7 additions & 4 deletions scanner/walker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import (
"sort"
"strings"
"testing"

"codemap/internal/projectpath"
)

func TestIgnoredDirs(t *testing.T) {
Expand Down Expand Up @@ -739,11 +741,12 @@ func TestGitIgnoreCacheEnsureDir(t *testing.T) {
}
cache := NewGitIgnoreCache(root)
cache.EnsureDir(sub)
if _, ok := cache.cache[sub]; !ok {
t.Fatalf("expected gitignore cache for %q", sub)
canonicalSub := projectpath.CanonicalPath(sub)
if _, ok := cache.cache[canonicalSub]; !ok {
t.Fatalf("expected gitignore cache for %q", canonicalSub)
}
if _, ok := cache.patterns[sub]; !ok {
t.Fatalf("expected gitignore patterns for %q", sub)
if _, ok := cache.patterns[canonicalSub]; !ok {
t.Fatalf("expected gitignore patterns for %q", canonicalSub)
}
if !cache.ShouldIgnore(filepath.Join(sub, "file.tmp")) {
t.Fatal("expected nested .gitignore pattern to apply")
Expand Down
Loading
Loading