diff --git a/cmd/stdio.go b/cmd/stdio.go index f12de5c..1354b08 100644 --- a/cmd/stdio.go +++ b/cmd/stdio.go @@ -30,6 +30,7 @@ import ( "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/ory/lumen/internal/config" "github.com/ory/lumen/internal/embedder" + "github.com/ory/lumen/internal/git" "github.com/ory/lumen/internal/index" "github.com/ory/lumen/internal/merkle" "github.com/spf13/cobra" @@ -136,8 +137,21 @@ type indexerCache struct { // path passes through a directory in merkle.SkipDirs (e.g. "testdata"). Such // a parent index would never contain path's files, so it is not useful. func (ic *indexerCache) findEffectiveRoot(path string) string { + // Cap the upward walk at the git repository root. This prevents lumen + // from adopting a large ancestor index (e.g. a GOPATH index) that + // happens to contain path as a subdirectory, which would cause + // EnsureFresh to scan the entire ancestor tree. + gitRoot, gitErr := git.RepoRoot(path) + candidate := filepath.Dir(path) for { + // Do not walk above the git repo root. + if gitErr == nil { + if rel, relErr := filepath.Rel(gitRoot, candidate); relErr != nil || strings.HasPrefix(rel, "..") { + break + } + } + if !pathCrossesSkipDir(candidate, path) { if _, ok := ic.cache[candidate]; ok { return candidate diff --git a/cmd/stdio_test.go b/cmd/stdio_test.go index 80e1c65..7d76013 100644 --- a/cmd/stdio_test.go +++ b/cmd/stdio_test.go @@ -17,6 +17,7 @@ package cmd import ( "context" "os" + "os/exec" "path/filepath" "strings" "sync" @@ -164,6 +165,50 @@ func TestIndexerCache_FindEffectiveRoot(t *testing.T) { }) } +func TestIndexerCache_FindEffectiveRoot_GitBoundary(t *testing.T) { + // Structure: ancestor/ (has an index DB) → repo/ (git root) → subdir/ + // findEffectiveRoot must not walk above the git repo root to adopt the + // ancestor index. + tmpDir := t.TempDir() + t.Setenv("XDG_DATA_HOME", tmpDir) + + const model = "test-model" + + // Create directory layout. + ancestor := filepath.Join(tmpDir, "ancestor") + repo := filepath.Join(ancestor, "repo") + subdir := filepath.Join(repo, "subdir") + for _, d := range []string{ancestor, repo, subdir} { + if err := os.MkdirAll(d, 0o755); err != nil { + t.Fatal(err) + } + } + + // Initialise a git repository at repo/. + cmd := exec.Command("git", "init", repo) + if out, err := cmd.CombinedOutput(); err != nil { + t.Fatalf("git init failed: %v\n%s", err, out) + } + + // Create a fake index DB for the ancestor directory (above the git root). + ancestorDBPath := config.DBPathForProject(ancestor, model) + if err := os.MkdirAll(filepath.Dir(ancestorDBPath), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(ancestorDBPath, []byte{}, 0o644); err != nil { + t.Fatal(err) + } + + ic := &indexerCache{ + cache: make(map[string]cacheEntry), + model: model, + } + root := ic.findEffectiveRoot(subdir) + if root != subdir { + t.Fatalf("expected findEffectiveRoot to stop at git boundary and return %s, got %s", subdir, root) + } +} + func TestIndexerCache_GetOrCreate_ReusesParentIndex(t *testing.T) { tmpDir := t.TempDir() t.Setenv("XDG_DATA_HOME", tmpDir) diff --git a/internal/git/worktree.go b/internal/git/worktree.go index 0557a84..a3883a7 100644 --- a/internal/git/worktree.go +++ b/internal/git/worktree.go @@ -93,6 +93,28 @@ func InternalWorktreePaths(projectPath string) []string { return result } +// RepoRoot returns the absolute path of the root directory of the git +// repository containing projectPath, by running "git rev-parse --show-toplevel". +// Returns an error if git is not available or projectPath is not inside a git +// repository. +func RepoRoot(projectPath string) (string, error) { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + cmd := exec.CommandContext(ctx, "git", "rev-parse", "--show-toplevel") + cmd.Dir = projectPath + out, err := cmd.Output() + if err != nil { + return "", err + } + + root := strings.TrimSpace(string(out)) + if resolved, err := filepath.EvalSymlinks(root); err == nil { + root = resolved + } + return root, nil +} + // ListWorktrees returns the absolute paths of all worktrees (including the // main working tree) for the repository containing projectPath. Returns nil // if git is not available or projectPath is not inside a git repository.