mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-30 01:51:20 +00:00
fix(hooks): prefix Windows device names with an extension in hook slot names (#3374)
Trigger: on Windows, storage-slot.ts sanitizeSlotBasename prefixes a device-name basename that has an extension (`CON.txt` -> `repository-CON.txt`), but the hook's copy in registry-query.cjs only matched the bare device name. With GITNEXUS_STORAGE_ROOT set, a checkout named e.g. `con.txt` got a different slot from the hook than from the CLI, so the hook could not see its index. Fix: mirror the platform branch (extension form on win32, bare form elsewhere) in all four byte-identical registry-query.cjs copies, and point their header comments at storage-slot.ts. Add a hook-vs-TS parity test over device-name basenames on both stubbed platforms, and fix the byte-identity test title to say four copies. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
3e9035e322
commit
ef4776c10c
5 changed files with 105 additions and 21 deletions
|
|
@ -218,11 +218,11 @@ function branchSlug(rawRef) {
|
|||
return `${safe}-${hash}`;
|
||||
}
|
||||
|
||||
// Mirror gitnexus/src/storage/storage-resolver.ts storageSlotName exactly
|
||||
// Mirror gitnexus/src/storage/storage-slot.ts slotNameForCanonicalPath exactly
|
||||
// (sanitize + sha256 of the canonical repo path, 12-hex suffix).
|
||||
function sanitizeSlotBasename(value) {
|
||||
// Cap first, then walk the tail once — same order as
|
||||
// gitnexus/src/storage/storage-resolver.ts (avoids /[. ]+$/ ReDoS).
|
||||
// gitnexus/src/storage/storage-slot.ts (avoids /[. ]+$/ ReDoS).
|
||||
const sanitized = value.replace(/[\u0000-\u001f<>:"/\\|?*]/g, '-').slice(0, 80);
|
||||
let end = sanitized.length;
|
||||
while (end > 0) {
|
||||
|
|
@ -231,9 +231,13 @@ function sanitizeSlotBasename(value) {
|
|||
end--;
|
||||
}
|
||||
const candidate = sanitized.slice(0, end) || 'repository';
|
||||
return /^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/i.test(candidate)
|
||||
? `repository-${candidate}`
|
||||
: candidate;
|
||||
// Windows also reserves device names with an extension (`CON.txt`); same
|
||||
// platform branch as gitnexus/src/storage/storage-slot.ts.
|
||||
const reserved =
|
||||
process.platform === 'win32'
|
||||
? /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(\..*)?$/i
|
||||
: /^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/i;
|
||||
return reserved.test(candidate) ? `repository-${candidate}` : candidate;
|
||||
}
|
||||
|
||||
function storageSlotName(repoPath) {
|
||||
|
|
|
|||
|
|
@ -218,11 +218,11 @@ function branchSlug(rawRef) {
|
|||
return `${safe}-${hash}`;
|
||||
}
|
||||
|
||||
// Mirror gitnexus/src/storage/storage-resolver.ts storageSlotName exactly
|
||||
// Mirror gitnexus/src/storage/storage-slot.ts slotNameForCanonicalPath exactly
|
||||
// (sanitize + sha256 of the canonical repo path, 12-hex suffix).
|
||||
function sanitizeSlotBasename(value) {
|
||||
// Cap first, then walk the tail once — same order as
|
||||
// gitnexus/src/storage/storage-resolver.ts (avoids /[. ]+$/ ReDoS).
|
||||
// gitnexus/src/storage/storage-slot.ts (avoids /[. ]+$/ ReDoS).
|
||||
const sanitized = value.replace(/[\u0000-\u001f<>:"/\\|?*]/g, '-').slice(0, 80);
|
||||
let end = sanitized.length;
|
||||
while (end > 0) {
|
||||
|
|
@ -231,9 +231,13 @@ function sanitizeSlotBasename(value) {
|
|||
end--;
|
||||
}
|
||||
const candidate = sanitized.slice(0, end) || 'repository';
|
||||
return /^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/i.test(candidate)
|
||||
? `repository-${candidate}`
|
||||
: candidate;
|
||||
// Windows also reserves device names with an extension (`CON.txt`); same
|
||||
// platform branch as gitnexus/src/storage/storage-slot.ts.
|
||||
const reserved =
|
||||
process.platform === 'win32'
|
||||
? /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(\..*)?$/i
|
||||
: /^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/i;
|
||||
return reserved.test(candidate) ? `repository-${candidate}` : candidate;
|
||||
}
|
||||
|
||||
function storageSlotName(repoPath) {
|
||||
|
|
|
|||
|
|
@ -218,11 +218,11 @@ function branchSlug(rawRef) {
|
|||
return `${safe}-${hash}`;
|
||||
}
|
||||
|
||||
// Mirror gitnexus/src/storage/storage-resolver.ts storageSlotName exactly
|
||||
// Mirror gitnexus/src/storage/storage-slot.ts slotNameForCanonicalPath exactly
|
||||
// (sanitize + sha256 of the canonical repo path, 12-hex suffix).
|
||||
function sanitizeSlotBasename(value) {
|
||||
// Cap first, then walk the tail once — same order as
|
||||
// gitnexus/src/storage/storage-resolver.ts (avoids /[. ]+$/ ReDoS).
|
||||
// gitnexus/src/storage/storage-slot.ts (avoids /[. ]+$/ ReDoS).
|
||||
const sanitized = value.replace(/[\u0000-\u001f<>:"/\\|?*]/g, '-').slice(0, 80);
|
||||
let end = sanitized.length;
|
||||
while (end > 0) {
|
||||
|
|
@ -231,9 +231,13 @@ function sanitizeSlotBasename(value) {
|
|||
end--;
|
||||
}
|
||||
const candidate = sanitized.slice(0, end) || 'repository';
|
||||
return /^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/i.test(candidate)
|
||||
? `repository-${candidate}`
|
||||
: candidate;
|
||||
// Windows also reserves device names with an extension (`CON.txt`); same
|
||||
// platform branch as gitnexus/src/storage/storage-slot.ts.
|
||||
const reserved =
|
||||
process.platform === 'win32'
|
||||
? /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(\..*)?$/i
|
||||
: /^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/i;
|
||||
return reserved.test(candidate) ? `repository-${candidate}` : candidate;
|
||||
}
|
||||
|
||||
function storageSlotName(repoPath) {
|
||||
|
|
|
|||
|
|
@ -218,11 +218,11 @@ function branchSlug(rawRef) {
|
|||
return `${safe}-${hash}`;
|
||||
}
|
||||
|
||||
// Mirror gitnexus/src/storage/storage-resolver.ts storageSlotName exactly
|
||||
// Mirror gitnexus/src/storage/storage-slot.ts slotNameForCanonicalPath exactly
|
||||
// (sanitize + sha256 of the canonical repo path, 12-hex suffix).
|
||||
function sanitizeSlotBasename(value) {
|
||||
// Cap first, then walk the tail once — same order as
|
||||
// gitnexus/src/storage/storage-resolver.ts (avoids /[. ]+$/ ReDoS).
|
||||
// gitnexus/src/storage/storage-slot.ts (avoids /[. ]+$/ ReDoS).
|
||||
const sanitized = value.replace(/[\u0000-\u001f<>:"/\\|?*]/g, '-').slice(0, 80);
|
||||
let end = sanitized.length;
|
||||
while (end > 0) {
|
||||
|
|
@ -231,9 +231,13 @@ function sanitizeSlotBasename(value) {
|
|||
end--;
|
||||
}
|
||||
const candidate = sanitized.slice(0, end) || 'repository';
|
||||
return /^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/i.test(candidate)
|
||||
? `repository-${candidate}`
|
||||
: candidate;
|
||||
// Windows also reserves device names with an extension (`CON.txt`); same
|
||||
// platform branch as gitnexus/src/storage/storage-slot.ts.
|
||||
const reserved =
|
||||
process.platform === 'win32'
|
||||
? /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(\..*)?$/i
|
||||
: /^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/i;
|
||||
return reserved.test(candidate) ? `repository-${candidate}` : candidate;
|
||||
}
|
||||
|
||||
function storageSlotName(repoPath) {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import os from 'os';
|
|||
import path from 'path';
|
||||
import { createRequire } from 'module';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import { slotNameForCanonicalPath } from '../../src/storage/storage-slot.js';
|
||||
|
||||
/**
|
||||
* #3352 — the Claude hook resolves a shared-store checkout to the commit
|
||||
|
|
@ -96,7 +97,7 @@ describe('registry-query shared store graph (#3352)', () => {
|
|||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('keeps the three hook copies byte-identical', () => {
|
||||
it('keeps the four hook copies byte-identical', () => {
|
||||
const [primary, ...copies] = HOOK_COPIES.map((f) => fs.readFileSync(f, 'utf-8'));
|
||||
for (const copy of copies) expect(copy).toBe(primary);
|
||||
});
|
||||
|
|
@ -124,3 +125,70 @@ describe('registry-query shared store graph (#3352)', () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* #3374 — the hook's slot name must match `slotNameForCanonicalPath` for
|
||||
* device-name basenames on both platform branches, or a GITNEXUS_STORAGE_ROOT
|
||||
* index is invisible to the hook. The checkout itself is never created (the
|
||||
* hook falls back to the resolved path); only the slot is written. A real
|
||||
* Windows host cannot create the POSIX-branch slot (`CON.txt-<hash>`), so the
|
||||
* stubbed rows run on POSIX hosts, which exercise both branches.
|
||||
*/
|
||||
describe.skipIf(process.platform === 'win32')('registry-query slot name parity (#3374)', () => {
|
||||
const realPlatform = process.platform;
|
||||
const savedHome = process.env.GITNEXUS_HOME;
|
||||
const savedStoragePath = process.env.GITNEXUS_STORAGE_PATH;
|
||||
const savedStorageRoot = process.env.GITNEXUS_STORAGE_ROOT;
|
||||
let tmp: string;
|
||||
|
||||
beforeEach(() => {
|
||||
tmp = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'gn-hook-slot-')));
|
||||
process.env.GITNEXUS_HOME = path.join(tmp, 'home');
|
||||
process.env.GITNEXUS_STORAGE_ROOT = path.join(tmp, 'root');
|
||||
delete process.env.GITNEXUS_STORAGE_PATH;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
Object.defineProperty(process, 'platform', { value: realPlatform, configurable: true });
|
||||
if (savedHome === undefined) delete process.env.GITNEXUS_HOME;
|
||||
else process.env.GITNEXUS_HOME = savedHome;
|
||||
if (savedStoragePath === undefined) delete process.env.GITNEXUS_STORAGE_PATH;
|
||||
else process.env.GITNEXUS_STORAGE_PATH = savedStoragePath;
|
||||
if (savedStorageRoot === undefined) delete process.env.GITNEXUS_STORAGE_ROOT;
|
||||
else process.env.GITNEXUS_STORAGE_ROOT = savedStorageRoot;
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
const basenames = [
|
||||
'CON',
|
||||
'con.txt',
|
||||
'NUL.tar.gz',
|
||||
'COM1',
|
||||
'LPT9.log',
|
||||
'aux',
|
||||
'prn.',
|
||||
'normal',
|
||||
'CONSOLE',
|
||||
'com0.txt',
|
||||
];
|
||||
const rows = (['win32', 'linux'] as const).flatMap((platform) =>
|
||||
basenames.map((basename) => [platform, basename] as const),
|
||||
);
|
||||
|
||||
it.each(rows)('on %s resolves the slot storage-slot.ts names for %s', (platform, basename) => {
|
||||
Object.defineProperty(process, 'platform', { value: platform, configurable: true });
|
||||
const checkout = path.join(tmp, 'repos', basename);
|
||||
const slot = path.join(tmp, 'root', slotNameForCanonicalPath(checkout));
|
||||
fs.mkdirSync(slot, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(slot, 'gitnexus.json'),
|
||||
JSON.stringify({ repoPath: checkout, storagePath: slot, lastCommit: 'abc' }),
|
||||
);
|
||||
fs.mkdirSync(path.join(tmp, 'home'), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(tmp, 'home', 'registry.json'),
|
||||
JSON.stringify([{ name: basename, path: checkout, indexedAt: '', lastCommit: '' }]),
|
||||
);
|
||||
expect(load(HOOK_COPIES[0]).findRegisteredRepo(checkout)?.storagePath).toBe(slot);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue