From e1d8c65f8c02eb343f8e079a3750587448aa96bd Mon Sep 17 00:00:00 2001 From: weiyf Date: Wed, 26 Aug 2026 16:53:09 +0800 Subject: [PATCH] fix(watch): normalize SSH repo identity paths --- .../core/auto-sync/analysis-worker-launch.ts | 10 ++++++++-- gitnexus/src/core/auto-sync/config.ts | 6 +++++- gitnexus/src/server/git-clone.ts | 4 ++++ .../unit/auto-sync-analysis-worker.test.ts | 13 ++++++++++++- gitnexus/test/unit/auto-sync.test.ts | 6 ++++++ gitnexus/test/unit/file-lock.test.ts | 19 ++++++++++++++++--- gitnexus/test/unit/git-clone.test.ts | 9 +++++++++ 7 files changed, 60 insertions(+), 7 deletions(-) diff --git a/gitnexus/src/core/auto-sync/analysis-worker-launch.ts b/gitnexus/src/core/auto-sync/analysis-worker-launch.ts index b92aaaa84..29060b85e 100644 --- a/gitnexus/src/core/auto-sync/analysis-worker-launch.ts +++ b/gitnexus/src/core/auto-sync/analysis-worker-launch.ts @@ -5,6 +5,7 @@ import path from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; import type { AnalyzeOptions, AnalyzeResult } from '../run-analyze.js'; import type { WorkerMessage } from '../../server/analyze-worker.js'; +import { autoHeapCapMb } from '../ingestion/utils/effective-ram.js'; const _require = createRequire(import.meta.url); const TERMINATION_GRACE_MS = 10_000; @@ -58,9 +59,14 @@ export function createAutoSyncAnalysisRunner( reject(new Error(`Auto-sync analyze worker is missing: ${workerPath}`)); return; } + const workerHeapMb = Math.min(8192, autoHeapCapMb()); const execArgv = isDev - ? ['--import', pathToFileURL(_require.resolve('tsx/esm')).href, '--max-old-space-size=8192'] - : ['--max-old-space-size=8192']; + ? [ + '--import', + pathToFileURL(_require.resolve('tsx/esm')).href, + `--max-old-space-size=${workerHeapMb}`, + ] + : [`--max-old-space-size=${workerHeapMb}`]; const child = deps.forkWorker(workerPath, execArgv); child.stdout?.resume(); child.stderr?.resume(); diff --git a/gitnexus/src/core/auto-sync/config.ts b/gitnexus/src/core/auto-sync/config.ts index 230df86c1..02bd5a7c1 100644 --- a/gitnexus/src/core/auto-sync/config.ts +++ b/gitnexus/src/core/auto-sync/config.ts @@ -246,7 +246,11 @@ export function parseAutoSyncConfig(content: string, configPath: string): AutoSy } export function validateAutoSyncRemoteUrl(remoteUrl: string): void { - const match = /^git@([^:\s/]+):([^\s]+)$/.exec(remoteUrl.trim()); + const trimmed = remoteUrl.trim(); + if (trimmed.includes('?') || trimmed.includes('#')) { + throw new Error('must not include query strings or fragments'); + } + const match = /^git@([^:\s/]+):([^\s]+)$/.exec(trimmed); if (!match) { throw new Error( 'must use an SSH URL on github.com, gitlab.com, or gitee.com', diff --git a/gitnexus/src/server/git-clone.ts b/gitnexus/src/server/git-clone.ts index 7598068ad..e902b7061 100644 --- a/gitnexus/src/server/git-clone.ts +++ b/gitnexus/src/server/git-clone.ts @@ -117,6 +117,10 @@ export function validateGitUrl(url: string): void { throw new Error('Only https:// and http:// git URLs are allowed'); } + if (parsed.search || parsed.hash) { + throw new Error('Git URLs must not include query strings or fragments'); + } + const host = parsed.hostname.toLowerCase(); // Block known dangerous hostnames (cloud metadata services) diff --git a/gitnexus/test/unit/auto-sync-analysis-worker.test.ts b/gitnexus/test/unit/auto-sync-analysis-worker.test.ts index 63a78ade4..d6a7b42d6 100644 --- a/gitnexus/test/unit/auto-sync-analysis-worker.test.ts +++ b/gitnexus/test/unit/auto-sync-analysis-worker.test.ts @@ -1,5 +1,11 @@ import { EventEmitter } from 'node:events'; import { describe, expect, it, vi } from 'vitest'; + +const { autoHeapCapMbMock } = vi.hoisted(() => ({ autoHeapCapMbMock: vi.fn(() => 512) })); +vi.mock('../../src/core/ingestion/utils/effective-ram.js', () => ({ + autoHeapCapMb: autoHeapCapMbMock, +})); + import { createAutoSyncAnalysisRunner } from '../../src/core/auto-sync/analysis-worker-launch.js'; describe('auto-sync analysis worker', () => { @@ -10,9 +16,14 @@ describe('auto-sync analysis worker', () => { stdout: { resume: vi.fn() }, stderr: { resume: vi.fn() }, }); - const run = createAutoSyncAnalysisRunner({ forkWorker: vi.fn(() => child as any) }); + const forkWorker = vi.fn(() => child as any); + const run = createAutoSyncAnalysisRunner({ forkWorker }); const result = run('/tmp/repo', { branch: 'main' }, 50); + expect(forkWorker).toHaveBeenCalledWith( + expect.any(String), + expect.arrayContaining(['--max-old-space-size=512']), + ); child.emit('message', { type: 'progress', phase: 'parsing', progress: 20 }); child.emit('message', { type: 'complete', result: { stats: { files: 3 } } }); child.emit('exit', 0, null); diff --git a/gitnexus/test/unit/auto-sync.test.ts b/gitnexus/test/unit/auto-sync.test.ts index 00ac48ded..668671761 100644 --- a/gitnexus/test/unit/auto-sync.test.ts +++ b/gitnexus/test/unit/auto-sync.test.ts @@ -452,6 +452,12 @@ describe('auto-sync', () => { expect(() => validateAutoSyncRemoteUrl('git@example.com:owner/repo.git')).toThrow( 'host must be', ); + expect(() => validateAutoSyncRemoteUrl('git@github.com:owner/repo.git?ref=main')).toThrow( + 'must not include query strings or fragments', + ); + expect(() => validateAutoSyncRemoteUrl('git@github.com:owner/repo.git#main')).toThrow( + 'must not include query strings or fragments', + ); }); it('parses repo git timeout durations', () => { diff --git a/gitnexus/test/unit/file-lock.test.ts b/gitnexus/test/unit/file-lock.test.ts index aba2a2ac5..8c2a41abd 100644 --- a/gitnexus/test/unit/file-lock.test.ts +++ b/gitnexus/test/unit/file-lock.test.ts @@ -84,11 +84,24 @@ describe('file lock', () => { await nextRelease(); }); - it('fails closed for a legacy or invalid lock without owner metadata', async () => { + it('fails closed for legacy or invalid lock contents without owner metadata', async () => { const lockPath = await tempLockPath(); - await fs.mkdir(lockPath, { recursive: true }); + const invalidContents = ['legacy lock', '{not json', JSON.stringify({ pid: 123 })]; + await fs.mkdir(path.dirname(lockPath), { recursive: true }); - await expect(acquireFileLock(lockPath)).rejects.toBeInstanceOf(FileLockBusyError); + for (const content of invalidContents) { + await fs.writeFile(lockPath, content, 'utf-8'); + await expect( + acquireFileLock(lockPath, { pid: 456, processStartTime: 'next-start' }), + ).rejects.toBeInstanceOf(FileLockBusyError); + await expect(fs.readFile(lockPath, 'utf-8')).resolves.toBe(content); + await fs.rm(lockPath); + } + + await fs.mkdir(lockPath, { recursive: true }); + await expect( + acquireFileLock(lockPath, { pid: 456, processStartTime: 'next-start' }), + ).rejects.toBeInstanceOf(FileLockBusyError); await expect(fs.access(lockPath)).resolves.toBeUndefined(); }); diff --git a/gitnexus/test/unit/git-clone.test.ts b/gitnexus/test/unit/git-clone.test.ts index cb05792ae..0c365e359 100644 --- a/gitnexus/test/unit/git-clone.test.ts +++ b/gitnexus/test/unit/git-clone.test.ts @@ -197,6 +197,15 @@ describe('git-clone', () => { expect(() => validateGitUrl('http://gitlab.com/user/repo.git')).not.toThrow(); }); + it('rejects query strings and fragments instead of reinterpreting clone remotes', () => { + expect(() => validateGitUrl('https://github.com/user/repo.git?ref=main')).toThrow( + 'must not include query strings or fragments', + ); + expect(() => validateGitUrl('https://github.com/user/repo.git#main')).toThrow( + 'must not include query strings or fragments', + ); + }); + it('blocks SSH protocol', () => { expect(() => validateGitUrl('ssh://git@github.com/user/repo.git')).toThrow( 'Only https:// and http://',