gitnexus 1.6.4-rc.61 → 1.6.4-rc.63
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.
- package/dist/core/run-analyze.js +19 -3
- package/dist/server/api.js +2 -2
- package/dist/storage/git.d.ts +50 -0
- package/dist/storage/git.js +77 -0
- package/dist/storage/repo-manager.js +20 -2
- package/package.json +1 -1
package/dist/core/run-analyze.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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);
|
package/dist/server/api.js
CHANGED
|
@@ -1190,7 +1190,7 @@ export const createServer = async (port, host = '127.0.0.1') => {
|
|
|
1190
1190
|
});
|
|
1191
1191
|
// ── Analyze API ──────────────────────────────────────────────────────
|
|
1192
1192
|
// POST /api/analyze — start a new analysis job
|
|
1193
|
-
app.post('/api/analyze', async (req, res) => {
|
|
1193
|
+
app.post('/api/analyze', createRouteLimiter({ limit: 10 }), async (req, res) => {
|
|
1194
1194
|
try {
|
|
1195
1195
|
const { url: repoUrl, path: repoLocalPath, force, embeddings, dropEmbeddings } = req.body;
|
|
1196
1196
|
// Input type validation
|
|
@@ -1433,7 +1433,7 @@ export const createServer = async (port, host = '127.0.0.1') => {
|
|
|
1433
1433
|
// ── Embedding endpoints ────────────────────────────────────────────
|
|
1434
1434
|
const embedJobManager = new JobManager();
|
|
1435
1435
|
// POST /api/embed — trigger server-side embedding generation
|
|
1436
|
-
app.post('/api/embed', async (req, res) => {
|
|
1436
|
+
app.post('/api/embed', createRouteLimiter({ limit: 20 }), async (req, res) => {
|
|
1437
1437
|
try {
|
|
1438
1438
|
const entry = await resolveRepo(requestedRepo(req));
|
|
1439
1439
|
if (!entry) {
|
package/dist/storage/git.d.ts
CHANGED
|
@@ -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
|
*
|
package/dist/storage/git.js
CHANGED
|
@@ -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
|
-
|
|
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