Skip to content

Commit 10e9635

Browse files
authored
fix(index): cap parent index walk at git repository boundary (#56)
Prevents lumen from adopting ancestor indexes outside the current git repository (e.g. a GOPATH-level index) when EnsureFresh is called from a subdirectory. Without this cap, the upward walk in findEffectiveRoot could reach indexes containing the entire workspace, causing excessive scanning and high GPU/fan usage.
1 parent 7b4f58c commit 10e9635

File tree

3 files changed

+81
-0
lines changed

3 files changed

+81
-0
lines changed

cmd/stdio.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import (
3030
"github.com/modelcontextprotocol/go-sdk/mcp"
3131
"github.com/ory/lumen/internal/config"
3232
"github.com/ory/lumen/internal/embedder"
33+
"github.com/ory/lumen/internal/git"
3334
"github.com/ory/lumen/internal/index"
3435
"github.com/ory/lumen/internal/merkle"
3536
"github.com/spf13/cobra"
@@ -136,8 +137,21 @@ type indexerCache struct {
136137
// path passes through a directory in merkle.SkipDirs (e.g. "testdata"). Such
137138
// a parent index would never contain path's files, so it is not useful.
138139
func (ic *indexerCache) findEffectiveRoot(path string) string {
140+
// Cap the upward walk at the git repository root. This prevents lumen
141+
// from adopting a large ancestor index (e.g. a GOPATH index) that
142+
// happens to contain path as a subdirectory, which would cause
143+
// EnsureFresh to scan the entire ancestor tree.
144+
gitRoot, gitErr := git.RepoRoot(path)
145+
139146
candidate := filepath.Dir(path)
140147
for {
148+
// Do not walk above the git repo root.
149+
if gitErr == nil {
150+
if rel, relErr := filepath.Rel(gitRoot, candidate); relErr != nil || strings.HasPrefix(rel, "..") {
151+
break
152+
}
153+
}
154+
141155
if !pathCrossesSkipDir(candidate, path) {
142156
if _, ok := ic.cache[candidate]; ok {
143157
return candidate

cmd/stdio_test.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ package cmd
1717
import (
1818
"context"
1919
"os"
20+
"os/exec"
2021
"path/filepath"
2122
"strings"
2223
"sync"
@@ -164,6 +165,50 @@ func TestIndexerCache_FindEffectiveRoot(t *testing.T) {
164165
})
165166
}
166167

168+
func TestIndexerCache_FindEffectiveRoot_GitBoundary(t *testing.T) {
169+
// Structure: ancestor/ (has an index DB) → repo/ (git root) → subdir/
170+
// findEffectiveRoot must not walk above the git repo root to adopt the
171+
// ancestor index.
172+
tmpDir := t.TempDir()
173+
t.Setenv("XDG_DATA_HOME", tmpDir)
174+
175+
const model = "test-model"
176+
177+
// Create directory layout.
178+
ancestor := filepath.Join(tmpDir, "ancestor")
179+
repo := filepath.Join(ancestor, "repo")
180+
subdir := filepath.Join(repo, "subdir")
181+
for _, d := range []string{ancestor, repo, subdir} {
182+
if err := os.MkdirAll(d, 0o755); err != nil {
183+
t.Fatal(err)
184+
}
185+
}
186+
187+
// Initialise a git repository at repo/.
188+
cmd := exec.Command("git", "init", repo)
189+
if out, err := cmd.CombinedOutput(); err != nil {
190+
t.Fatalf("git init failed: %v\n%s", err, out)
191+
}
192+
193+
// Create a fake index DB for the ancestor directory (above the git root).
194+
ancestorDBPath := config.DBPathForProject(ancestor, model)
195+
if err := os.MkdirAll(filepath.Dir(ancestorDBPath), 0o755); err != nil {
196+
t.Fatal(err)
197+
}
198+
if err := os.WriteFile(ancestorDBPath, []byte{}, 0o644); err != nil {
199+
t.Fatal(err)
200+
}
201+
202+
ic := &indexerCache{
203+
cache: make(map[string]cacheEntry),
204+
model: model,
205+
}
206+
root := ic.findEffectiveRoot(subdir)
207+
if root != subdir {
208+
t.Fatalf("expected findEffectiveRoot to stop at git boundary and return %s, got %s", subdir, root)
209+
}
210+
}
211+
167212
func TestIndexerCache_GetOrCreate_ReusesParentIndex(t *testing.T) {
168213
tmpDir := t.TempDir()
169214
t.Setenv("XDG_DATA_HOME", tmpDir)

internal/git/worktree.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,28 @@ func InternalWorktreePaths(projectPath string) []string {
9393
return result
9494
}
9595

96+
// RepoRoot returns the absolute path of the root directory of the git
97+
// repository containing projectPath, by running "git rev-parse --show-toplevel".
98+
// Returns an error if git is not available or projectPath is not inside a git
99+
// repository.
100+
func RepoRoot(projectPath string) (string, error) {
101+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
102+
defer cancel()
103+
104+
cmd := exec.CommandContext(ctx, "git", "rev-parse", "--show-toplevel")
105+
cmd.Dir = projectPath
106+
out, err := cmd.Output()
107+
if err != nil {
108+
return "", err
109+
}
110+
111+
root := strings.TrimSpace(string(out))
112+
if resolved, err := filepath.EvalSymlinks(root); err == nil {
113+
root = resolved
114+
}
115+
return root, nil
116+
}
117+
96118
// ListWorktrees returns the absolute paths of all worktrees (including the
97119
// main working tree) for the repository containing projectPath. Returns nil
98120
// if git is not available or projectPath is not inside a git repository.

0 commit comments

Comments
 (0)