gitnexus 1.6.10-rc.17 → 1.6.10-rc.18
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/server/analyze-launch.d.ts +6 -0
- package/dist/server/analyze-launch.js +82 -6
- package/dist/server/api.d.ts +30 -0
- package/dist/server/api.js +61 -19
- package/dist/storage/repo-manager.d.ts +12 -0
- package/dist/storage/repo-manager.js +12 -0
- package/package.json +1 -1
- package/scripts/cross-platform-tests.ts +5 -0
- package/web/assets/{index-B8sko6-6.js → index-B4eB4dNZ.js} +49 -49
- package/web/assets/{index-CzdH8ADg.css → index-CX_fADmQ.css} +1 -1
- package/web/index.html +2 -2
|
@@ -17,6 +17,12 @@ export interface LaunchDeps {
|
|
|
17
17
|
};
|
|
18
18
|
acquireRepoLock: (key: string) => string | null;
|
|
19
19
|
releaseRepoLock: (key: string) => void;
|
|
20
|
+
/**
|
|
21
|
+
* Drops the server's cached LadybugDB handle (closeLbug). The worker
|
|
22
|
+
* process rewrites the repo's DB files on disk, so a connection opened
|
|
23
|
+
* before the rewrite keeps reading the pre-rewrite state until evicted.
|
|
24
|
+
*/
|
|
25
|
+
closeDbHandle: () => Promise<void>;
|
|
20
26
|
}
|
|
21
27
|
export interface LaunchOptions {
|
|
22
28
|
force?: boolean;
|
|
@@ -10,16 +10,83 @@
|
|
|
10
10
|
* path is resolved relative to `import.meta.url`.
|
|
11
11
|
*/
|
|
12
12
|
import path from 'path';
|
|
13
|
+
import { existsSync, statSync } from 'node:fs';
|
|
13
14
|
import { fork } from 'child_process';
|
|
14
15
|
import { fileURLToPath, pathToFileURL } from 'url';
|
|
15
16
|
import { createRequire } from 'node:module';
|
|
16
|
-
import { getStoragePath } from '../storage/repo-manager.js';
|
|
17
|
+
import { canonicalizePath, getStoragePath, INDEX_METADATA_FILE, listRegisteredRepos, registryPathEquals, } from '../storage/repo-manager.js';
|
|
17
18
|
import { logger } from '../core/logger.js';
|
|
18
19
|
const _require = createRequire(import.meta.url);
|
|
19
20
|
const MAX_WORKER_RETRIES = 2;
|
|
21
|
+
/**
|
|
22
|
+
* The worker reports `complete` over IPC before its on-disk finalization
|
|
23
|
+
* (LadybugDB checkpoint + native handle release + metadata write) is visible
|
|
24
|
+
* at `getStoragePath(targetPath)` — observed up to ~6.5s behind the IPC
|
|
25
|
+
* message. Opening the database inside that window is what the pre-IPC
|
|
26
|
+
* ordering was meant to prevent and is actively dangerous: reads fail with
|
|
27
|
+
* binder errors or return an empty graph, the open can quarantine the
|
|
28
|
+
* in-flight WAL, and the native layer racing the rewrite has crashed the
|
|
29
|
+
* whole server (SIGSEGV-class exit, no output) on slow CI runners.
|
|
30
|
+
*/
|
|
31
|
+
const FINALIZE_SETTLE_TIMEOUT_MS = 60_000;
|
|
32
|
+
const FINALIZE_SETTLE_POLL_MS = 200;
|
|
33
|
+
/**
|
|
34
|
+
* Resolve once the analyzed repo's index is settled at `storagePath`: the
|
|
35
|
+
* LadybugDB file and metadata both exist AND were (re)written by THIS job
|
|
36
|
+
* (mtime >= jobStartMs — bare existence is not enough, a re-analysis leaves
|
|
37
|
+
* the previous index in place while it works), and no transient WAL/shadow/
|
|
38
|
+
* checkpoint sidecars remain (the worker's native close has finished).
|
|
39
|
+
*
|
|
40
|
+
* Never rejects. Timing out logs and proceeds (pre-gate behavior) rather
|
|
41
|
+
* than failing a job whose analysis genuinely succeeded — e.g. a no-op
|
|
42
|
+
* non-force analyze legitimately rewrites nothing.
|
|
43
|
+
*/
|
|
44
|
+
/**
|
|
45
|
+
* Look up the analyzed repo's registered storage path. The request's
|
|
46
|
+
* user-provided path is used only as a comparison key; the filesystem probes
|
|
47
|
+
* below run against the registry's own `storagePath` — the server-owned
|
|
48
|
+
* record readers resolve through, and not a user-controlled value
|
|
49
|
+
* (CodeQL js/path-injection).
|
|
50
|
+
*/
|
|
51
|
+
const registeredStoragePath = async (targetPath) => {
|
|
52
|
+
const target = canonicalizePath(path.resolve(targetPath));
|
|
53
|
+
const entries = await listRegisteredRepos();
|
|
54
|
+
const entry = entries.find((e) => registryPathEquals(canonicalizePath(e.path), target));
|
|
55
|
+
return entry?.storagePath ?? null;
|
|
56
|
+
};
|
|
57
|
+
const waitForSettledIndex = async (targetPath, jobStartMs) => {
|
|
58
|
+
const settled = (storagePath) => {
|
|
59
|
+
try {
|
|
60
|
+
const lbugStat = statSync(path.join(storagePath, 'lbug'));
|
|
61
|
+
const metaStat = statSync(path.join(storagePath, INDEX_METADATA_FILE));
|
|
62
|
+
return (lbugStat.mtimeMs >= jobStartMs &&
|
|
63
|
+
metaStat.mtimeMs >= jobStartMs &&
|
|
64
|
+
['lbug.wal', 'lbug.shadow', 'lbug.wal.checkpoint'].every((f) => !existsSync(path.join(storagePath, f))));
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return false; // not written yet
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
const deadline = Date.now() + FINALIZE_SETTLE_TIMEOUT_MS;
|
|
71
|
+
for (;;) {
|
|
72
|
+
// Re-resolved each round: the worker registers the repo as part of the
|
|
73
|
+
// finalization this gate is waiting out.
|
|
74
|
+
const storagePath = await registeredStoragePath(targetPath);
|
|
75
|
+
if (storagePath && settled(storagePath))
|
|
76
|
+
return;
|
|
77
|
+
if (Date.now() > deadline) {
|
|
78
|
+
logger.warn({ targetPath }, 'analyze finalization not visible after timeout; completing job anyway');
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
await new Promise((resolve) => setTimeout(resolve, FINALIZE_SETTLE_POLL_MS));
|
|
82
|
+
}
|
|
83
|
+
};
|
|
20
84
|
export function createLaunchAnalysisWorker(deps) {
|
|
21
|
-
const { jobManager, backend, acquireRepoLock, releaseRepoLock } = deps;
|
|
85
|
+
const { jobManager, backend, acquireRepoLock, releaseRepoLock, closeDbHandle } = deps;
|
|
22
86
|
return function launchAnalysisWorker(job, targetPath, opts) {
|
|
87
|
+
// For waitForSettledIndex: files (re)written by this job have mtimes at or
|
|
88
|
+
// after this instant. Taken before the fork so no worker write predates it.
|
|
89
|
+
const jobStartMs = Date.now();
|
|
23
90
|
// Acquire shared repo lock (keyed on storagePath to match embed handler)
|
|
24
91
|
const analyzeLockKey = getStoragePath(targetPath);
|
|
25
92
|
const lockErr = acquireRepoLock(analyzeLockKey);
|
|
@@ -67,10 +134,17 @@ export function createLaunchAnalysisWorker(deps) {
|
|
|
67
134
|
}
|
|
68
135
|
else if (msg.type === 'complete') {
|
|
69
136
|
releaseRepoLock(analyzeLockKey);
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
|
|
73
|
-
|
|
137
|
+
// Before marking complete: (1) wait for the worker's on-disk
|
|
138
|
+
// finalization to settle (see waitForSettledIndex), (2) evict the
|
|
139
|
+
// cached DB handle — same invalidation DELETE /api/repo performs, a
|
|
140
|
+
// handle opened before the rewrite reads pre-rewrite state — and
|
|
141
|
+
// only then (3) reinitialize the backend. This makes the ordering
|
|
142
|
+
// comment below true in practice: the repo is actually queryable
|
|
143
|
+
// when the client receives the SSE complete event.
|
|
144
|
+
waitForSettledIndex(targetPath, jobStartMs)
|
|
145
|
+
.then(() => closeDbHandle())
|
|
146
|
+
.catch(() => { }) // best-effort: eviction failure must not fail the job
|
|
147
|
+
.then(() => backend.init())
|
|
74
148
|
.then(() => {
|
|
75
149
|
jobManager.updateJob(job.id, { status: 'complete', repoName: msg.result.repoName });
|
|
76
150
|
})
|
|
@@ -84,6 +158,8 @@ export function createLaunchAnalysisWorker(deps) {
|
|
|
84
158
|
}
|
|
85
159
|
else if (msg.type === 'error') {
|
|
86
160
|
releaseRepoLock(analyzeLockKey);
|
|
161
|
+
// A failed (force) analyze may still have rewritten DB files first.
|
|
162
|
+
void closeDbHandle().catch(() => { });
|
|
87
163
|
jobManager.updateJob(job.id, { status: 'failed', error: msg.message });
|
|
88
164
|
}
|
|
89
165
|
});
|
package/dist/server/api.d.ts
CHANGED
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
* CORS is restricted to localhost, private/LAN networks, and the deployed site.
|
|
9
9
|
*/
|
|
10
10
|
import express from 'express';
|
|
11
|
+
import { type RegistryEntry } from '../storage/repo-manager.js';
|
|
11
12
|
import { type GraphNode, type GraphRelationship } from '../_shared/index.js';
|
|
13
|
+
import { JobManager } from './analyze-job.js';
|
|
12
14
|
/**
|
|
13
15
|
* Determine whether an HTTP Origin header value is allowed by CORS policy.
|
|
14
16
|
*
|
|
@@ -49,6 +51,34 @@ export declare const registerWebUI: (app: express.Express, staticDir: string | n
|
|
|
49
51
|
export declare const writeNdjsonRecord: (res: express.Response, record: GraphStreamRecord, signal?: AbortSignal) => Promise<void>;
|
|
50
52
|
export declare const getNodeQuery: (table: string, includeContent: boolean) => string;
|
|
51
53
|
export declare const streamGraphNdjson: (res: express.Response, includeContent?: boolean, signal?: AbortSignal) => Promise<void>;
|
|
54
|
+
/**
|
|
55
|
+
* Mount an SSE progress endpoint for a JobManager.
|
|
56
|
+
* Handles: initial state, terminal events, heartbeat, event IDs, client disconnect.
|
|
57
|
+
*
|
|
58
|
+
* Terminal payloads carry `repoPath` (the analyzed path) alongside the display
|
|
59
|
+
* `repoName` so clients can reconnect by path identity — with duplicate
|
|
60
|
+
* basenames, a name-only reconnect resolves to the first same-named sibling.
|
|
61
|
+
* Exported for unit tests that lock the wire payload shape.
|
|
62
|
+
*/
|
|
63
|
+
export declare const mountSSEProgress: (app: express.Express, routePath: string, jm: JobManager) => void;
|
|
64
|
+
/**
|
|
65
|
+
* Resolve a `?repo=` request param against the registry in two tiers:
|
|
66
|
+
*
|
|
67
|
+
* 1. Path claim — any input containing a separator ('/' or '\\', which
|
|
68
|
+
* cover path.sep on every platform) is treated as a path claim and
|
|
69
|
+
* resolved by canonical registry path ONLY. A miss fails closed
|
|
70
|
+
* (null, never a basename fallback) so a stale or wrong path can
|
|
71
|
+
* never silently retarget a same-named sibling repo (#2419).
|
|
72
|
+
* Within this tier, only absolute or Windows-shaped ('\\') claims
|
|
73
|
+
* are worth canonicalizing; relative claims like 'org/name' or
|
|
74
|
+
* './repo' are rejected immediately WITHOUT touching the filesystem
|
|
75
|
+
* — canonicalizing them would run an attacker-influenced
|
|
76
|
+
* CWD-relative realpathSync probe on un-rate-limited GET routes,
|
|
77
|
+
* and no legitimate caller sends relative paths.
|
|
78
|
+
* 2. Name fallback — bare names (no separators) keep the legacy
|
|
79
|
+
* basename/name match for older callers.
|
|
80
|
+
*/
|
|
81
|
+
export declare const resolveRegisteredRepoEntry: (repos: RegistryEntry[], repoName?: string) => RegistryEntry | null;
|
|
52
82
|
/**
|
|
53
83
|
* Handle a GET /api/file request body. Extracted from createServer's route
|
|
54
84
|
* registration so it can be unit-tested without spinning up an HTTP server
|
package/dist/server/api.js
CHANGED
|
@@ -12,7 +12,7 @@ import cors from 'cors';
|
|
|
12
12
|
import path from 'path';
|
|
13
13
|
import fs from 'fs/promises';
|
|
14
14
|
import { createRequire } from 'node:module';
|
|
15
|
-
import { loadMeta, listRegisteredRepos, getStoragePath } from '../storage/repo-manager.js';
|
|
15
|
+
import { canonicalizePath, cloneDirBelongsToEntry, loadMeta, listRegisteredRepos, getStoragePath, registryPathEquals, } from '../storage/repo-manager.js';
|
|
16
16
|
import { executeQuery, executePrepared, executeWithReusedStatement, streamQuery, flushWAL, closeLbug, withLbugDb, isReadOnlyDbError, } from '../core/lbug/lbug-adapter.js';
|
|
17
17
|
import { isValidQueryParams } from '../core/lbug/query-params.js';
|
|
18
18
|
import { NODE_TABLES } from '../_shared/index.js';
|
|
@@ -381,8 +381,13 @@ export const streamGraphNdjson = async (res, includeContent = false, signal) =>
|
|
|
381
381
|
/**
|
|
382
382
|
* Mount an SSE progress endpoint for a JobManager.
|
|
383
383
|
* Handles: initial state, terminal events, heartbeat, event IDs, client disconnect.
|
|
384
|
+
*
|
|
385
|
+
* Terminal payloads carry `repoPath` (the analyzed path) alongside the display
|
|
386
|
+
* `repoName` so clients can reconnect by path identity — with duplicate
|
|
387
|
+
* basenames, a name-only reconnect resolves to the first same-named sibling.
|
|
388
|
+
* Exported for unit tests that lock the wire payload shape.
|
|
384
389
|
*/
|
|
385
|
-
const mountSSEProgress = (app, routePath, jm) => {
|
|
390
|
+
export const mountSSEProgress = (app, routePath, jm) => {
|
|
386
391
|
app.get(routePath, (req, res) => {
|
|
387
392
|
let jobId;
|
|
388
393
|
try {
|
|
@@ -412,6 +417,7 @@ const mountSSEProgress = (app, routePath, jm) => {
|
|
|
412
417
|
eventId++;
|
|
413
418
|
res.write(`id: ${eventId}\nevent: ${job.status}\ndata: ${JSON.stringify({
|
|
414
419
|
repoName: job.repoName,
|
|
420
|
+
repoPath: job.repoPath,
|
|
415
421
|
error: job.error,
|
|
416
422
|
})}\n\n`);
|
|
417
423
|
res.end();
|
|
@@ -435,6 +441,7 @@ const mountSSEProgress = (app, routePath, jm) => {
|
|
|
435
441
|
const eventJob = jm.getJob(jobId);
|
|
436
442
|
res.write(`id: ${eventId}\nevent: ${progress.phase}\ndata: ${JSON.stringify({
|
|
437
443
|
repoName: eventJob?.repoName,
|
|
444
|
+
repoPath: eventJob?.repoPath,
|
|
438
445
|
error: eventJob?.error,
|
|
439
446
|
})}\n\n`);
|
|
440
447
|
clearInterval(heartbeat);
|
|
@@ -477,6 +484,43 @@ const requestedRepo = (req) => {
|
|
|
477
484
|
}
|
|
478
485
|
return undefined;
|
|
479
486
|
};
|
|
487
|
+
const repoParamBasename = (repoName) => repoName.replace(/\\/g, '/').split('/').filter(Boolean).pop() ?? repoName;
|
|
488
|
+
/**
|
|
489
|
+
* Resolve a `?repo=` request param against the registry in two tiers:
|
|
490
|
+
*
|
|
491
|
+
* 1. Path claim — any input containing a separator ('/' or '\\', which
|
|
492
|
+
* cover path.sep on every platform) is treated as a path claim and
|
|
493
|
+
* resolved by canonical registry path ONLY. A miss fails closed
|
|
494
|
+
* (null, never a basename fallback) so a stale or wrong path can
|
|
495
|
+
* never silently retarget a same-named sibling repo (#2419).
|
|
496
|
+
* Within this tier, only absolute or Windows-shaped ('\\') claims
|
|
497
|
+
* are worth canonicalizing; relative claims like 'org/name' or
|
|
498
|
+
* './repo' are rejected immediately WITHOUT touching the filesystem
|
|
499
|
+
* — canonicalizing them would run an attacker-influenced
|
|
500
|
+
* CWD-relative realpathSync probe on un-rate-limited GET routes,
|
|
501
|
+
* and no legitimate caller sends relative paths.
|
|
502
|
+
* 2. Name fallback — bare names (no separators) keep the legacy
|
|
503
|
+
* basename/name match for older callers.
|
|
504
|
+
*/
|
|
505
|
+
export const resolveRegisteredRepoEntry = (repos, repoName) => {
|
|
506
|
+
if (!repoName)
|
|
507
|
+
return repos[0] ?? null;
|
|
508
|
+
const looksLikePath = path.isAbsolute(repoName) || repoName.includes('/') || repoName.includes('\\');
|
|
509
|
+
if (looksLikePath) {
|
|
510
|
+
// Relative path claims fail closed with zero filesystem probes.
|
|
511
|
+
if (!path.isAbsolute(repoName) && !repoName.includes('\\'))
|
|
512
|
+
return null;
|
|
513
|
+
const requestedPath = canonicalizePath(repoName);
|
|
514
|
+
const pathMatch = repos.find((r) => registryPathEquals(canonicalizePath(r.path), requestedPath));
|
|
515
|
+
if (pathMatch)
|
|
516
|
+
return pathMatch;
|
|
517
|
+
return null;
|
|
518
|
+
}
|
|
519
|
+
const normalizedName = repoParamBasename(repoName);
|
|
520
|
+
return (repos.find((r) => r.name === normalizedName) ||
|
|
521
|
+
repos.find((r) => r.name.toLowerCase() === normalizedName.toLowerCase()) ||
|
|
522
|
+
null);
|
|
523
|
+
};
|
|
480
524
|
/**
|
|
481
525
|
* Handle a GET /api/file request body. Extracted from createServer's route
|
|
482
526
|
* registration so it can be unit-tested without spinning up an HTTP server
|
|
@@ -703,6 +747,7 @@ export const createServer = async (port, host = '127.0.0.1') => {
|
|
|
703
747
|
backend,
|
|
704
748
|
acquireRepoLock,
|
|
705
749
|
releaseRepoLock,
|
|
750
|
+
closeDbHandle: closeLbug,
|
|
706
751
|
});
|
|
707
752
|
/**
|
|
708
753
|
* Maximum time the hold-queue will wait for an active analysis job to complete.
|
|
@@ -713,19 +758,8 @@ export const createServer = async (port, host = '127.0.0.1') => {
|
|
|
713
758
|
// Pass `req` to enable early exit if the client disconnects during the hold-queue wait.
|
|
714
759
|
const resolveRepo = async (repoName, isRetry = false, req) => {
|
|
715
760
|
const repos = await listRegisteredRepos();
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
// e.g. "C:\Users\LENOVO\.gitnexus\repos\todo.txt-cli" -> "todo.txt-cli"
|
|
719
|
-
const normalizedName = repoName ? path.basename(repoName) : undefined;
|
|
720
|
-
if (normalizedName) {
|
|
721
|
-
found =
|
|
722
|
-
repos.find((r) => r.name === normalizedName) ||
|
|
723
|
-
repos.find((r) => r.name.toLowerCase() === normalizedName.toLowerCase()) ||
|
|
724
|
-
null;
|
|
725
|
-
}
|
|
726
|
-
else if (repos.length > 0) {
|
|
727
|
-
found = repos[0]; // default to first repo
|
|
728
|
-
}
|
|
761
|
+
const found = resolveRegisteredRepoEntry(repos, repoName);
|
|
762
|
+
const normalizedName = repoName ? repoParamBasename(repoName) : undefined;
|
|
729
763
|
// If not yet in the registry, check whether a background job is actively cloning or
|
|
730
764
|
// analyzing this repo. Hold the connection open (up to 5 minutes) until it completes.
|
|
731
765
|
// We only wait for in-progress jobs ('queued'|'cloning'|'analyzing') — a 'complete' job
|
|
@@ -758,7 +792,7 @@ export const createServer = async (port, host = '127.0.0.1') => {
|
|
|
758
792
|
if (currentJob.status === 'complete') {
|
|
759
793
|
await backend.init();
|
|
760
794
|
const freshRepos = await listRegisteredRepos();
|
|
761
|
-
return freshRepos
|
|
795
|
+
return resolveRegisteredRepoEntry(freshRepos, repoName);
|
|
762
796
|
}
|
|
763
797
|
await new Promise((r) => setTimeout(r, 1000));
|
|
764
798
|
}
|
|
@@ -775,7 +809,7 @@ export const createServer = async (port, host = '127.0.0.1') => {
|
|
|
775
809
|
logger.debug({ repoName: String(normalizedName).replace(/[\r\n]/g, ' ') }, '[debug] resolveRepo 404, triggering deep init');
|
|
776
810
|
}
|
|
777
811
|
await backend.init();
|
|
778
|
-
return await resolveRepo(
|
|
812
|
+
return await resolveRepo(repoName, true, req);
|
|
779
813
|
}
|
|
780
814
|
return found;
|
|
781
815
|
};
|
|
@@ -827,6 +861,7 @@ export const createServer = async (port, host = '127.0.0.1') => {
|
|
|
827
861
|
res.json(repos.map((r) => ({
|
|
828
862
|
name: r.name,
|
|
829
863
|
path: r.path,
|
|
864
|
+
repoPath: r.path,
|
|
830
865
|
indexedAt: r.indexedAt,
|
|
831
866
|
lastCommit: r.lastCommit,
|
|
832
867
|
stats: r.stats,
|
|
@@ -837,7 +872,11 @@ export const createServer = async (port, host = '127.0.0.1') => {
|
|
|
837
872
|
}
|
|
838
873
|
});
|
|
839
874
|
// Get repo info
|
|
840
|
-
|
|
875
|
+
// Rate-limited (CodeQL js/missing-rate-limiting): resolveRepo canonicalizes
|
|
876
|
+
// the attacker-supplied ?repo= param (realpathSync probe for absolute /
|
|
877
|
+
// Windows-shaped claims). Default 60 rpm/IP — web callers hit this route
|
|
878
|
+
// only on connect/switch, never in a polling loop.
|
|
879
|
+
app.get('/api/repo', createRouteLimiter(), async (req, res) => {
|
|
841
880
|
try {
|
|
842
881
|
const entry = await resolveRepo(requestedRepo(req), false, req);
|
|
843
882
|
if (!entry) {
|
|
@@ -907,7 +946,10 @@ export const createServer = async (port, host = '127.0.0.1') => {
|
|
|
907
946
|
catch {
|
|
908
947
|
/* repo name not eligible for a clone dir (local repo) */
|
|
909
948
|
}
|
|
910
|
-
|
|
949
|
+
// Only remove the clone dir when it is *this* entry's path — a local
|
|
950
|
+
// repo registered under the same name would otherwise take a cloned
|
|
951
|
+
// sibling's checkout down with it (see cloneDirBelongsToEntry).
|
|
952
|
+
if (cloneDir && cloneDirBelongsToEntry(cloneDir, entry.path)) {
|
|
911
953
|
try {
|
|
912
954
|
const stat = await fs.stat(cloneDir);
|
|
913
955
|
if (stat.isDirectory()) {
|
|
@@ -54,6 +54,18 @@ export declare const canonicalizePath: (p: string) => string;
|
|
|
54
54
|
* lookups/dedup/finalize checks all share so they answer identically.
|
|
55
55
|
*/
|
|
56
56
|
export declare const registryPathEquals: (a: string, b: string) => boolean;
|
|
57
|
+
/**
|
|
58
|
+
* Does the clone dir derived from an entry's *name* actually belong to that
|
|
59
|
+
* entry? Registry names are not unique across storage locations: a cloned
|
|
60
|
+
* repo under `~/.gitnexus/repos/<name>` and a local repo registered under the
|
|
61
|
+
* same name share a `getCloneDir(entry.name)` result. The server's delete
|
|
62
|
+
* handler must therefore never remove the clone dir based on the name alone —
|
|
63
|
+
* only when the entry's own `path` resolves to that dir (mirroring its step-2b
|
|
64
|
+
* rule that cleanup is driven off `entry.path`, so a same-named sibling's
|
|
65
|
+
* clone is never removed). Both sides are canonicalised so symlinked or
|
|
66
|
+
* differently-spelled forms of the same dir still match.
|
|
67
|
+
*/
|
|
68
|
+
export declare const cloneDirBelongsToEntry: (cloneDir: string, entryPath: string) => boolean;
|
|
57
69
|
export interface RepoMeta {
|
|
58
70
|
repoPath: string;
|
|
59
71
|
lastCommit: string;
|
|
@@ -71,6 +71,18 @@ export const canonicalizePath = (p) => {
|
|
|
71
71
|
* lookups/dedup/finalize checks all share so they answer identically.
|
|
72
72
|
*/
|
|
73
73
|
export const registryPathEquals = (a, b) => process.platform === 'win32' ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
74
|
+
/**
|
|
75
|
+
* Does the clone dir derived from an entry's *name* actually belong to that
|
|
76
|
+
* entry? Registry names are not unique across storage locations: a cloned
|
|
77
|
+
* repo under `~/.gitnexus/repos/<name>` and a local repo registered under the
|
|
78
|
+
* same name share a `getCloneDir(entry.name)` result. The server's delete
|
|
79
|
+
* handler must therefore never remove the clone dir based on the name alone —
|
|
80
|
+
* only when the entry's own `path` resolves to that dir (mirroring its step-2b
|
|
81
|
+
* rule that cleanup is driven off `entry.path`, so a same-named sibling's
|
|
82
|
+
* clone is never removed). Both sides are canonicalised so symlinked or
|
|
83
|
+
* differently-spelled forms of the same dir still match.
|
|
84
|
+
*/
|
|
85
|
+
export const cloneDirBelongsToEntry = (cloneDir, entryPath) => registryPathEquals(canonicalizePath(cloneDir), canonicalizePath(entryPath));
|
|
74
86
|
/**
|
|
75
87
|
* Bumped whenever incremental-indexing invariants change incompatibly.
|
|
76
88
|
* v2: `BasicBlock.callees` column added (statement-precise inter-procedural
|
package/package.json
CHANGED
|
@@ -74,6 +74,11 @@ const PLATFORM_LOGIC = [
|
|
|
74
74
|
// (macos), so the header parsing is proven on genuine binaries, not synthetic
|
|
75
75
|
// buffers (the ubuntu suite covers the ELF path).
|
|
76
76
|
'test/integration/extension-binary-real.test.ts',
|
|
77
|
+
// Server repo resolver branches on path shape (path.isAbsolute, backslash
|
|
78
|
+
// detection) and canonicalizePath/realpathSync, all of which differ between
|
|
79
|
+
// POSIX and Windows — the fail-closed path-claim semantics must hold on the
|
|
80
|
+
// real windows-latest path implementation (#2419/#2420).
|
|
81
|
+
'test/unit/server-api-repo-resolution.test.ts',
|
|
77
82
|
];
|
|
78
83
|
|
|
79
84
|
// Native LadybugDB integration tests — exercise the @ladybugdb/core
|