worktree.ts 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115
  1. /**
  2. * Git Worktree Awareness
  3. *
  4. * A CodeGraph index lives in a `.codegraph/` directory and is resolved by
  5. * walking up parent directories to the nearest one (see
  6. * `findNearestCodeGraphRoot`). That walk is unaware of git worktrees: when a
  7. * worktree is created *inside* the main checkout (e.g. some tools place them
  8. * under `.gitignore`d paths like `.claude/worktrees/<name>/`), a command run
  9. * from the worktree walks up and silently resolves the MAIN checkout's index.
  10. *
  11. * Every query then returns results from the main tree's code — usually a
  12. * different branch — rather than the worktree the user is actually editing.
  13. * Symbols added or changed only in the worktree are invisible. This module
  14. * detects that "borrowed index" situation so callers can warn about it.
  15. *
  16. * Detection is best-effort: when git is unavailable or the path isn't a repo,
  17. * it reports "no mismatch" and callers carry on unchanged.
  18. */
  19. import * as fs from 'fs';
  20. import * as path from 'path';
  21. import { execFileSync } from 'child_process';
  22. /**
  23. * Absolute, symlink-resolved toplevel of the git working tree that `dir`
  24. * belongs to, or null when `dir` isn't inside a git repo (or git is missing).
  25. *
  26. * `git rev-parse --show-toplevel` returns the per-worktree root: the main
  27. * checkout and each linked worktree report their own distinct directory, which
  28. * is exactly the distinction this module relies on.
  29. */
  30. export function gitWorktreeRoot(dir: string): string | null {
  31. try {
  32. const out = execFileSync('git', ['rev-parse', '--show-toplevel'], {
  33. cwd: dir,
  34. encoding: 'utf8',
  35. stdio: ['ignore', 'pipe', 'ignore'],
  36. windowsHide: true,
  37. }).trim();
  38. return out ? realpath(out) : null;
  39. } catch {
  40. return null;
  41. }
  42. }
  43. export interface WorktreeIndexMismatch {
  44. /** The git working tree the command was run from. */
  45. worktreeRoot: string;
  46. /** The (different) working tree whose `.codegraph` index is being used. */
  47. indexRoot: string;
  48. }
  49. /**
  50. * Detect when `startPath` lives in one git working tree but the resolved
  51. * CodeGraph index (`indexRoot`) belongs to a *different* working tree.
  52. *
  53. * Returns null — meaning "nothing to warn about" — when:
  54. * - `startPath` isn't in a git repo (or git is unavailable),
  55. * - the index already lives in `startPath`'s own working tree, or
  56. * - `indexRoot` isn't itself a working-tree root (an unrelated parent dir
  57. * that merely happens to contain a `.codegraph/`), which keeps non-git
  58. * and monorepo-subdir layouts from producing false warnings.
  59. */
  60. export function detectWorktreeIndexMismatch(
  61. startPath: string,
  62. indexRoot: string,
  63. ): WorktreeIndexMismatch | null {
  64. const worktreeRoot = gitWorktreeRoot(startPath);
  65. if (!worktreeRoot) return null;
  66. const resolvedIndexRoot = realpath(indexRoot);
  67. if (worktreeRoot === resolvedIndexRoot) return null;
  68. // Only flag it when the index root is itself a real working-tree root. This
  69. // distinguishes "borrowed another worktree's index" from "index sits in a
  70. // plain ancestor directory", and avoids warning outside git entirely.
  71. if (gitWorktreeRoot(resolvedIndexRoot) !== resolvedIndexRoot) return null;
  72. return { worktreeRoot, indexRoot: resolvedIndexRoot };
  73. }
  74. /** One-line-per-fact warning describing a detected mismatch. */
  75. export function worktreeMismatchWarning(m: WorktreeIndexMismatch): string {
  76. return (
  77. `This CodeGraph index belongs to a different git working tree.\n` +
  78. ` Running in: ${m.worktreeRoot}\n` +
  79. ` Index from: ${m.indexRoot}\n` +
  80. `Results reflect that tree's code (often a different branch), not this worktree — ` +
  81. `symbols changed only here are missing. Run "codegraph init -i" in this worktree ` +
  82. `for a worktree-local index.`
  83. );
  84. }
  85. /**
  86. * Compact, single-line variant for prefixing a tool's result. Read tools
  87. * return their answer inline, so the heads-up has to ride on the same payload
  88. * the agent is already reading — a multi-line block would bury the result.
  89. */
  90. export function worktreeMismatchNotice(m: WorktreeIndexMismatch): string {
  91. return (
  92. `⚠ CodeGraph results below come from a different git worktree (${m.indexRoot}), ` +
  93. `not where you're working (${m.worktreeRoot}) — they may reflect another branch, ` +
  94. `and symbols changed only here are missing. Run "codegraph init -i" here for a ` +
  95. `worktree-local index.`
  96. );
  97. }
  98. /** Resolve symlinks where possible so tmp/realpath quirks don't break equality. */
  99. function realpath(p: string): string {
  100. try {
  101. return fs.realpathSync(path.resolve(p));
  102. } catch {
  103. return path.resolve(p);
  104. }
  105. }