gitnexus 1.6.4-rc.61 → 1.6.4-rc.62

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -14,7 +14,7 @@ import { runPipelineFromRepo } from './ingestion/pipeline.js';
14
14
  import { initLbug, loadGraphToLbug, getLbugStats, executeQuery, executeWithReusedStatement, closeLbug, loadCachedEmbeddings, } from './lbug/lbug-adapter.js';
15
15
  import { createSearchFTSIndexes } from './search/fts-indexes.js';
16
16
  import { getStoragePaths, saveMeta, loadMeta, ensureGitNexusIgnored, registerRepo, cleanupOldKuzuFiles, } from '../storage/repo-manager.js';
17
- import { getCurrentCommit, getRemoteUrl, hasGitDir, getInferredRepoName } from '../storage/git.js';
17
+ import { getCurrentCommit, getRemoteUrl, hasGitDir, getInferredRepoName, resolveRepoIdentityRoot, } from '../storage/git.js';
18
18
  import { generateAIContextFiles } from '../cli/ai-context.js';
19
19
  import { EMBEDDING_TABLE_NAME } from './lbug/schema.js';
20
20
  import { STALE_HASH_SENTINEL } from './lbug/schema.js';
@@ -71,7 +71,12 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
71
71
  if (currentCommit !== '') {
72
72
  await ensureGitNexusIgnored(repoPath);
73
73
  return {
74
- repoName: options.registryName ?? getInferredRepoName(repoPath) ?? path.basename(repoPath),
74
+ // `resolveRepoIdentityRoot` collapses worktree roots to the
75
+ // canonical repo basename (#1259) but leaves arbitrary subdirs
76
+ // and `--skip-git` paths unchanged (#1232/#1233 intent preserved).
77
+ repoName: options.registryName ??
78
+ getInferredRepoName(repoPath) ??
79
+ path.basename(resolveRepoIdentityRoot(repoPath)),
75
80
  repoPath,
76
81
  stats: existingMeta.stats ?? {},
77
82
  alreadyUpToDate: true,
@@ -219,7 +224,18 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
219
224
  }
220
225
  }
221
226
  const { readServerMapping } = await import('./embeddings/server-mapping.js');
222
- const projectName = path.basename(repoPath);
227
+ // Mirror the registry's name-resolution chain so the server-mapping
228
+ // lookup key stays aligned with the final registry name (#1259):
229
+ // --name → remote-derived → canonical-root basename
230
+ // (preserved-alias is intentionally NOT consulted here — server
231
+ // mappings are addressed by the operationally-meaningful name the
232
+ // user configures, not by a sticky registry-only alias they may not
233
+ // know about. The previous canonical-only logic ignored both --name
234
+ // and remote-derived names, silently breaking server-mapping for
235
+ // anyone with a `--name` alias or remote-named repo.)
236
+ const projectName = options.registryName ??
237
+ getInferredRepoName(repoPath) ??
238
+ path.basename(resolveRepoIdentityRoot(repoPath));
223
239
  const serverName = await readServerMapping(projectName);
224
240
  const embeddingResult = await runEmbeddingPipeline(executeQuery, executeWithReusedStatement, (p) => {
225
241
  const scaled = 90 + Math.round((p.percent / 100) * 8);
@@ -28,6 +28,56 @@ export declare const getRemoteUrl: (repoPath: string) => string | undefined;
28
28
  * Find the git repository root from any path inside the repo
29
29
  */
30
30
  export declare const getGitRoot: (fromPath: string) => string | null;
31
+ /**
32
+ * Get the *canonical* repository root, dereferencing git worktrees.
33
+ *
34
+ * Unlike `getGitRoot` (which uses `git rev-parse --show-toplevel` and
35
+ * returns the WORKTREE's root when called inside a linked worktree),
36
+ * this uses `git rev-parse --git-common-dir` — the shared `.git`
37
+ * directory, identical for the main checkout and every linked
38
+ * worktree — and returns its parent.
39
+ *
40
+ * Why it matters (#1259): when `gitnexus analyze` runs inside a
41
+ * worktree (e.g. `/repo/wt-feature/`), deriving `repoName` from
42
+ * `path.basename(getGitRoot(cwd))` registers the project under the
43
+ * worktree's directory slug (`wt-feature`) instead of the canonical
44
+ * repo's basename (`repo`). Each worktree then re-registers as a
45
+ * "different" project, AGENTS.md is rewritten with the wrong MCP URI,
46
+ * and Claude-Code-style worktree workflows silently accumulate
47
+ * duplicate registry entries.
48
+ *
49
+ * Returns `null` when the path is not inside a git repository or
50
+ * `git` is not available, so callers can chain safely:
51
+ * `getCanonicalRepoRoot(p) ?? getGitRoot(p) ?? p`.
52
+ *
53
+ * `--path-format=absolute` is required because `--git-common-dir`
54
+ * returns a path *relative to cwd* by default (e.g. `../.git` when
55
+ * called from a worktree), which would resolve to the wrong absolute
56
+ * path if the caller later resolved it from a different directory.
57
+ */
58
+ export declare const getCanonicalRepoRoot: (fromPath: string) => string | null;
59
+ /**
60
+ * Resolve `fromPath` to the directory whose basename should drive the
61
+ * registry name (#1259) — the *identity root*. Three outcomes:
62
+ *
63
+ * 1. `fromPath` IS the canonical checkout root → returns it unchanged.
64
+ * 2. `fromPath` is a linked-worktree root (has its own `.git` entry, but
65
+ * `git rev-parse --git-common-dir` points at a different `.git`) →
66
+ * returns the canonical repo root.
67
+ * 3. `fromPath` is anything else — an arbitrary subdir under a git repo,
68
+ * a non-git folder, a `--skip-git` subdir of an unrelated parent
69
+ * checkout — returns `fromPath` unchanged.
70
+ *
71
+ * Why not just use `getCanonicalRepoRoot` directly? Because `git rev-parse
72
+ * --git-common-dir` resolves the same canonical root for ANY path inside
73
+ * a git repo, including unrelated subdirs. Using it for registry-name
74
+ * derivation would silently re-key a `--skip-git` subdir analyze under
75
+ * the parent git's basename, defeating the user's `--skip-git` intent
76
+ * (regressing the #1232/#1233 fix). The "is this path a tree root"
77
+ * gate confines the canonical-root collapse to exactly the cases where
78
+ * #1259 matters: main checkouts and linked worktrees.
79
+ */
80
+ export declare const resolveRepoIdentityRoot: (fromPath: string) => string;
31
81
  /**
32
82
  * Find a git root by checking only `.git` entries on the ancestor chain.
33
83
  *
@@ -91,6 +91,83 @@ export const getGitRoot = (fromPath) => {
91
91
  return null;
92
92
  }
93
93
  };
94
+ /**
95
+ * Get the *canonical* repository root, dereferencing git worktrees.
96
+ *
97
+ * Unlike `getGitRoot` (which uses `git rev-parse --show-toplevel` and
98
+ * returns the WORKTREE's root when called inside a linked worktree),
99
+ * this uses `git rev-parse --git-common-dir` — the shared `.git`
100
+ * directory, identical for the main checkout and every linked
101
+ * worktree — and returns its parent.
102
+ *
103
+ * Why it matters (#1259): when `gitnexus analyze` runs inside a
104
+ * worktree (e.g. `/repo/wt-feature/`), deriving `repoName` from
105
+ * `path.basename(getGitRoot(cwd))` registers the project under the
106
+ * worktree's directory slug (`wt-feature`) instead of the canonical
107
+ * repo's basename (`repo`). Each worktree then re-registers as a
108
+ * "different" project, AGENTS.md is rewritten with the wrong MCP URI,
109
+ * and Claude-Code-style worktree workflows silently accumulate
110
+ * duplicate registry entries.
111
+ *
112
+ * Returns `null` when the path is not inside a git repository or
113
+ * `git` is not available, so callers can chain safely:
114
+ * `getCanonicalRepoRoot(p) ?? getGitRoot(p) ?? p`.
115
+ *
116
+ * `--path-format=absolute` is required because `--git-common-dir`
117
+ * returns a path *relative to cwd* by default (e.g. `../.git` when
118
+ * called from a worktree), which would resolve to the wrong absolute
119
+ * path if the caller later resolved it from a different directory.
120
+ */
121
+ export const getCanonicalRepoRoot = (fromPath) => {
122
+ try {
123
+ const commonDir = execSync('git rev-parse --path-format=absolute --git-common-dir', {
124
+ cwd: fromPath,
125
+ stdio: ['ignore', 'pipe', 'ignore'],
126
+ })
127
+ .toString()
128
+ .trim();
129
+ if (!commonDir)
130
+ return null;
131
+ // Common dir is `<repo>/.git` for both the main checkout and all
132
+ // linked worktrees. Its parent is the canonical repo root.
133
+ return path.dirname(path.resolve(commonDir));
134
+ }
135
+ catch {
136
+ return null;
137
+ }
138
+ };
139
+ /**
140
+ * Resolve `fromPath` to the directory whose basename should drive the
141
+ * registry name (#1259) — the *identity root*. Three outcomes:
142
+ *
143
+ * 1. `fromPath` IS the canonical checkout root → returns it unchanged.
144
+ * 2. `fromPath` is a linked-worktree root (has its own `.git` entry, but
145
+ * `git rev-parse --git-common-dir` points at a different `.git`) →
146
+ * returns the canonical repo root.
147
+ * 3. `fromPath` is anything else — an arbitrary subdir under a git repo,
148
+ * a non-git folder, a `--skip-git` subdir of an unrelated parent
149
+ * checkout — returns `fromPath` unchanged.
150
+ *
151
+ * Why not just use `getCanonicalRepoRoot` directly? Because `git rev-parse
152
+ * --git-common-dir` resolves the same canonical root for ANY path inside
153
+ * a git repo, including unrelated subdirs. Using it for registry-name
154
+ * derivation would silently re-key a `--skip-git` subdir analyze under
155
+ * the parent git's basename, defeating the user's `--skip-git` intent
156
+ * (regressing the #1232/#1233 fix). The "is this path a tree root"
157
+ * gate confines the canonical-root collapse to exactly the cases where
158
+ * #1259 matters: main checkouts and linked worktrees.
159
+ */
160
+ export const resolveRepoIdentityRoot = (fromPath) => {
161
+ const resolved = path.resolve(fromPath);
162
+ const canonical = getCanonicalRepoRoot(resolved);
163
+ if (!canonical)
164
+ return resolved; // non-git → use as-is
165
+ if (canonical === resolved)
166
+ return canonical; // canonical checkout
167
+ if (hasGitDir(resolved))
168
+ return canonical; // linked worktree (has .git file)
169
+ return resolved; // arbitrary subdir under a git repo → preserve as-is
170
+ };
94
171
  /**
95
172
  * Find a git root by checking only `.git` entries on the ancestor chain.
96
173
  *
@@ -9,7 +9,7 @@ import fs from 'fs/promises';
9
9
  import { realpathSync } from 'fs';
10
10
  import path from 'path';
11
11
  import os from 'os';
12
- import { getInferredRepoName } from './git.js';
12
+ import { getInferredRepoName, resolveRepoIdentityRoot } from './git.js';
13
13
  /**
14
14
  * Normalise a repo path for registry comparison across platforms
15
15
  * (#664 review feedback from @evander-wang).
@@ -296,6 +296,18 @@ const hasCustomAlias = (entry, inferredName) => {
296
296
  const resolved = path.resolve(entry.path);
297
297
  if (entry.name === path.basename(resolved))
298
298
  return false;
299
+ // Canonical-root-derived names are not user aliases either (#1259):
300
+ // a worktree registered under the canonical repo's basename
301
+ // (e.g. `{name: 'repo', path: '/repo/wt-feature'}`) must re-register
302
+ // cleanly without firing the duplicate-name collision guard. Without
303
+ // this check `entry.name = 'repo'` !== `path.basename('/repo/wt-feature') = 'wt-feature'`,
304
+ // so the prior check returns true → `isPreservedAlias = true` → guard
305
+ // throws `RegistryNameCollisionError` against the also-registered
306
+ // canonical checkout entry. The Claude-Code per-task worktree workflow
307
+ // — analyze canonical, then analyze worktree, then re-analyze worktree
308
+ // — would break on the third call.
309
+ if (entry.name === path.basename(resolveRepoIdentityRoot(resolved)))
310
+ return false;
299
311
  if (inferredName && entry.name === inferredName)
300
312
  return false;
301
313
  return true;
@@ -372,7 +384,13 @@ export const registerRepo = async (repoPath, meta, opts) => {
372
384
  isPreservedAlias = true;
373
385
  }
374
386
  else {
375
- name = inferred ?? path.basename(resolved);
387
+ // Canonical-root fallback: when `resolved` is a worktree root,
388
+ // derive the registry name from the canonical repo's basename, not
389
+ // the worktree slug — see #1259. `resolveRepoIdentityRoot` confines
390
+ // the collapse to canonical checkouts and linked worktree roots only,
391
+ // so `--skip-git` subdirs of unrelated parent git repos keep using
392
+ // their own basename (preserves the #1232/#1233 fix's intent).
393
+ name = inferred ?? path.basename(resolveRepoIdentityRoot(resolved));
376
394
  }
377
395
  }
378
396
  // Duplicate-name guard: only fire when the user EXPLICITLY asked for
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitnexus",
3
- "version": "1.6.4-rc.61",
3
+ "version": "1.6.4-rc.62",
4
4
  "description": "Graph-powered code intelligence for AI agents. Index any codebase, query via MCP or CLI.",
5
5
  "author": "Abhigyan Patwari",
6
6
  "license": "PolyForm-Noncommercial-1.0.0",