Repository navigation
Expand file tree
/
Copy pathworktree.ts
More file actions
218 lines (200 loc) · 9 KB
/
Copy pathworktree.ts
File metadata and controls
218 lines (200 loc) · 9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
/**
* Git Worktree Awareness
*
* A CodeGraph index lives in a `.codegraph/` directory and is resolved by
* walking up parent directories to the nearest one (see
* `findNearestCodeGraphRoot`). That walk is unaware of git worktrees: when a
* worktree is created *inside* the main checkout (e.g. some tools place them
* under `.gitignore`d paths like `.claude/worktrees/<name>/`), a command run
* from the worktree walks up and silently resolves the MAIN checkout's index.
*
* Every query then returns results from the main tree's code — usually a
* different branch — rather than the worktree the user is actually editing.
* Symbols added or changed only in the worktree are invisible. This module
* detects that "borrowed index" situation so callers can warn about it.
*
* Detection is best-effort: when git is unavailable or the path isn't a repo,
* it reports "no mismatch" and callers carry on unchanged.
*/
import * as fs from 'fs';
import * as path from 'path';
import { execFileSync } from 'child_process';
/**
* Absolute, symlink-resolved toplevel of the git working tree that `dir`
* belongs to, or null when `dir` isn't inside a git repo (or git is missing).
*
* `git rev-parse --show-toplevel` returns the per-worktree root: the main
* checkout and each linked worktree report their own distinct directory, which
* is exactly the distinction this module relies on.
*/
export function gitWorktreeRoot(dir: string): string | null {
try {
const out = execFileSync('git', ['rev-parse', '--show-toplevel'], {
cwd: dir,
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'ignore'],
windowsHide: true,
// Bounded like every git call in extraction/: this runs (memoized) on
// the daemon's main event loop, where an unbounded hang would trip the
// 60s liveness watchdog and SIGKILL a healthy daemon (#1139).
timeout: 5000,
}).trim();
return out ? realpath(out) : null;
} catch {
return null;
}
}
/**
* Absolute, symlink-resolved git **common** directory for `dir` — the shared
* `.git` that all worktrees of one repository point at. Linked worktrees of the
* same repo report the SAME common dir; a submodule or an embedded clone is a
* DIFFERENT repository and reports its own (`…/.git/modules/<name>` or its own
* `.git`). That distinction is what separates a genuine "borrowed worktree"
* from a nested repo the parent index already covers. Null when not a repo.
*/
export function gitCommonDir(dir: string): string | null {
try {
const out = execFileSync('git', ['rev-parse', '--git-common-dir'], {
cwd: dir,
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'ignore'],
windowsHide: true,
timeout: 5000, // same rationale as gitWorktreeRoot
}).trim();
if (!out) return null;
// `--git-common-dir` is relative to cwd unless already absolute.
return realpath(path.isAbsolute(out) ? out : path.resolve(dir, out));
} catch {
return null;
}
}
export interface WorktreeIndexMismatch {
/** The git working tree the command was run from. */
worktreeRoot: string;
/** The (different) working tree whose `.codegraph` index is being used. */
indexRoot: string;
}
/**
* Detect when `startPath` lives in one git working tree but the resolved
* CodeGraph index (`indexRoot`) belongs to a *different* working tree.
*
* Returns null — meaning "nothing to warn about" — when:
* - `startPath` isn't in a git repo (or git is unavailable),
* - the index already lives in `startPath`'s own working tree, or
* - `indexRoot` isn't itself a working-tree root (an unrelated parent dir
* that merely happens to contain a `.codegraph/`), which keeps non-git
* and monorepo-subdir layouts from producing false warnings.
*/
export function detectWorktreeIndexMismatch(
startPath: string,
indexRoot: string,
): WorktreeIndexMismatch | null {
const worktreeRoot = gitWorktreeRoot(startPath);
if (!worktreeRoot) return null;
const resolvedIndexRoot = realpath(indexRoot);
if (worktreeRoot === resolvedIndexRoot) return null;
// Only flag it when the index root is itself a real working-tree root. This
// distinguishes "borrowed another worktree's index" from "index sits in a
// plain ancestor directory", and avoids warning outside git entirely.
if (gitWorktreeRoot(resolvedIndexRoot) !== resolvedIndexRoot) return null;
// Don't flag a nested repo (submodule / embedded clone) that `indexRoot`'s
// index ALREADY covers: indexing a super-repo descends into its submodules
// and gitlinked clones, so a query run from inside one resolves up to the
// parent index — whose graph *does* contain that nested repo's files. The
// warning's premise ("results are a different branch; symbols changed only
// here are missing") is false there, and its "run codegraph init -i" advice
// would needlessly fragment the unified workspace index. A genuine borrowed
// worktree and the index root are the SAME repository (they share a git
// common dir); a submodule/embedded clone is a DIFFERENT repository and does
// not — so suppress only when the two clearly differ. (#1031, #1033)
const worktreeCommon = gitCommonDir(worktreeRoot);
const indexCommon = gitCommonDir(resolvedIndexRoot);
if (worktreeCommon && indexCommon && worktreeCommon !== indexCommon) return null;
return { worktreeRoot, indexRoot: resolvedIndexRoot };
}
/** A git repository of its own nested below an index root. */
export interface NestedRepository {
/** The nested repository's working-tree root. */
root: string;
/** `root` relative to the index root, POSIX — the prefix its files carry in that index. */
relPath: string;
}
/**
* The repository `startPath` belongs to when it is a DIFFERENT git repository
* nested strictly below `indexRoot` — an embedded clone, a submodule, a
* gitignored checkout. The up-walk to the nearest `.codegraph/` crosses its
* boundary without noticing, and the ancestor's index may or may not hold its
* files, so the caller has to ask that index before answering for it (#2110).
*
* Returns null — nothing to check — when:
* - `startPath` isn't in a git repo (or git is unavailable),
* - its repository root is the index root or above it (an ordinary
* subdirectory; a monorepo sub-project with its own index), or
* - it is a linked worktree of the index root's OWN repository (same git
* common dir): the #155 borrowed-worktree case, warned about, not refused.
*/
export function nestedRepositoryBelow(startPath: string, indexRoot: string): NestedRepository | null {
// git needs an existing directory to run in: a file, or a sub-path that does
// not exist yet, belongs to the repository of its nearest existing directory.
let dir = path.resolve(startPath);
while (!isDirectory(dir)) {
const up = path.dirname(dir);
if (up === dir) return null;
dir = up;
}
const repoRoot = gitWorktreeRoot(dir);
if (!repoRoot) return null;
// The on-disk spelling of both ends (`realpathSync.native` normalizes case on
// macOS and Windows), so `relPath` matches the paths the index stored.
const rel = path.relative(nativeRealpath(indexRoot), nativeRealpath(repoRoot));
if (!rel || rel === '..' || rel.startsWith(`..${path.sep}`) || path.isAbsolute(rel)) return null;
const repoCommon = gitCommonDir(repoRoot);
if (!repoCommon || repoCommon === gitCommonDir(realpath(indexRoot))) return null;
return { root: repoRoot, relPath: rel.split(path.sep).join('/') };
}
function isDirectory(p: string): boolean {
try {
return fs.statSync(p).isDirectory();
} catch {
return false;
}
}
function nativeRealpath(p: string): string {
try {
return fs.realpathSync.native(path.resolve(p));
} catch {
return realpath(p);
}
}
/** One-line-per-fact warning describing a detected mismatch. */
export function worktreeMismatchWarning(m: WorktreeIndexMismatch): string {
return (
`This CodeGraph index belongs to a different git working tree.\n` +
` Running in: ${m.worktreeRoot}\n` +
` Index from: ${m.indexRoot}\n` +
`Results reflect that tree's code (often a different branch), not this worktree — ` +
`symbols changed only here are missing. Run "codegraph init -i" in this worktree ` +
`for a worktree-local index.`
);
}
/**
* Compact, single-line variant for prefixing a tool's result. Read tools
* return their answer inline, so the heads-up has to ride on the same payload
* the agent is already reading — a multi-line block would bury the result.
*/
export function worktreeMismatchNotice(m: WorktreeIndexMismatch): string {
return (
`⚠ CodeGraph results below come from a different git worktree (${m.indexRoot}), ` +
`not where you're working (${m.worktreeRoot}) — they may reflect another branch, ` +
`and symbols changed only here are missing. Run "codegraph init -i" here for a ` +
`worktree-local index.`
);
}
/** Resolve symlinks where possible so tmp/realpath quirks don't break equality. */
function realpath(p: string): string {
try {
return fs.realpathSync(path.resolve(p));
} catch {
return path.resolve(p);
}
}