mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-09 03:17:54 +00:00
fix: address PR review cleanup
This commit is contained in:
parent
60b00c3d74
commit
9f49eea84e
12 changed files with 184 additions and 96 deletions
|
|
@ -19,14 +19,14 @@
|
|||
*
|
||||
* False-positive suppression:
|
||||
* - Skips calls whose receiver is a known non-tree-sitter library (`JSON`,
|
||||
* `URL`, `marked`, `Number`).
|
||||
* `URL`, `marked`, `Number`, `path`).
|
||||
* - Skips calls whose first argument is a string-literal (grammar-load smoke
|
||||
* tests like `_testParser.parse('service X { rpc Y (R) returns (R); }')`).
|
||||
* - Skips test files (`.test.ts`/`.test.tsx`/`.spec.ts`).
|
||||
* - Skips the `safe-parse.ts` helper itself.
|
||||
*/
|
||||
|
||||
const SKIPPED_RECEIVERS = new Set(['JSON', 'URL', 'marked', 'Number', 'Math']);
|
||||
const SKIPPED_RECEIVERS = new Set(['JSON', 'URL', 'marked', 'Number', 'Math', 'path']);
|
||||
|
||||
export default {
|
||||
meta: {
|
||||
|
|
@ -74,7 +74,7 @@ export default {
|
|||
// Receiver-text-shape skip: anything matching well-known JS APIs that
|
||||
// happen to have a `.parse(<expr>)` shape but aren't tree-sitter.
|
||||
if (
|
||||
/^(JSON|URL|marked|Number|Math|Date|globalThis\.JSON)\b/.test(receiverText) ||
|
||||
/^(JSON|URL|marked|Number|Math|Date|path|globalThis\.JSON)\b/.test(receiverText) ||
|
||||
/\bjson\.parse\b/i.test(receiverText)
|
||||
) {
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -68,9 +68,13 @@ async function initWatchConfig(): Promise<void> {
|
|||
const configPath = getAutoSyncConfigPath();
|
||||
try {
|
||||
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
||||
await fs.writeFile(configPath, defaultSyncConfig(path.resolve(path.dirname(configPath), 'repo')), {
|
||||
flag: 'wx',
|
||||
});
|
||||
await fs.writeFile(
|
||||
configPath,
|
||||
defaultSyncConfig(path.resolve(path.dirname(configPath), 'repo')),
|
||||
{
|
||||
flag: 'wx',
|
||||
},
|
||||
);
|
||||
} catch (err: unknown) {
|
||||
if ((err as NodeJS.ErrnoException).code === 'EEXIST') {
|
||||
process.stderr.write(`[auto-sync] Config already exists: ${configPath}\n`);
|
||||
|
|
|
|||
|
|
@ -209,7 +209,9 @@ export function parseAutoSyncConfig(content: string, configPath: string): AutoSy
|
|||
export function validateAutoSyncRemoteUrl(remoteUrl: string): void {
|
||||
const match = /^git@([^:\s/]+):([^\s]+)$/.exec(remoteUrl.trim());
|
||||
if (!match) {
|
||||
throw new Error('must use git@github.com:owner/repo.git, git@gitlab.com:group/repo.git, or git@gitee.com:owner/repo.git');
|
||||
throw new Error(
|
||||
'must use git@github.com:owner/repo.git, git@gitlab.com:group/repo.git, or git@gitee.com:owner/repo.git',
|
||||
);
|
||||
}
|
||||
const host = match[1].toLowerCase();
|
||||
const repoPath = match[2];
|
||||
|
|
@ -223,7 +225,8 @@ export function validateAutoSyncRemoteUrl(remoteUrl: string): void {
|
|||
|
||||
export function validateAutoSyncBranchName(branch: string): void {
|
||||
if (!branch.trim()) throw new Error('must not be empty');
|
||||
if (/[\s\0-\x1f\x7f]/.test(branch)) throw new Error('must not contain whitespace or control characters');
|
||||
if (/[\s\0-\x1f\x7f]/.test(branch))
|
||||
throw new Error('must not contain whitespace or control characters');
|
||||
if (/[~^:?*[\\]/.test(branch)) throw new Error('contains characters not allowed in a git ref');
|
||||
if (branch.startsWith('-')) throw new Error('must not start with "-"');
|
||||
if (branch.includes('..')) throw new Error('must not contain ".."');
|
||||
|
|
|
|||
|
|
@ -65,7 +65,11 @@ export async function resolveConfiguredCloneRoot(localPath: string): Promise<Aut
|
|||
await fs.mkdir(root, { recursive: true });
|
||||
await assertDirectoryOwnerAndPermissions(root);
|
||||
const realRoot = await fs.realpath(root);
|
||||
assertContainedOrSame(root, realRoot, 'Configured clone root realpath escaped its normalized path');
|
||||
assertContainedOrSame(
|
||||
root,
|
||||
realRoot,
|
||||
'Configured clone root realpath escaped its normalized path',
|
||||
);
|
||||
assertNotDangerousRoot(realRoot);
|
||||
assertNotGitNexusInternalRoot(realRoot);
|
||||
|
||||
|
|
@ -90,7 +94,10 @@ export function normalizeConfiguredCloneRoot(localPath: string): string {
|
|||
return resolved;
|
||||
}
|
||||
|
||||
export async function quarantineAutoSyncPartial(targetDir: string, quarantineRoot: string): Promise<string> {
|
||||
export async function quarantineAutoSyncPartial(
|
||||
targetDir: string,
|
||||
quarantineRoot: string,
|
||||
): Promise<string> {
|
||||
await fs.mkdir(quarantineRoot, { recursive: true, mode: 0o700 });
|
||||
const base = path.basename(targetDir);
|
||||
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
|
|
@ -119,7 +126,8 @@ function assertNotDangerousRoot(root: string): void {
|
|||
throw new Error(`Refusing unsafe auto-sync clone root under ${dangerousRoot}: ${root}`);
|
||||
}
|
||||
}
|
||||
if (path.parse(root).root === root) throw new Error(`Refusing filesystem root as clone root: ${root}`);
|
||||
if (path.parse(root).root === root)
|
||||
throw new Error(`Refusing filesystem root as clone root: ${root}`);
|
||||
}
|
||||
|
||||
function assertNotGitNexusInternalRoot(root: string): void {
|
||||
|
|
@ -151,7 +159,8 @@ async function assertNoSymlinkPath(root: string): Promise<void> {
|
|||
if ((err as NodeJS.ErrnoException).code === 'ENOENT') break;
|
||||
throw err;
|
||||
}
|
||||
if (stat.isSymbolicLink()) throw new Error(`Refusing symlink in auto-sync clone root path: ${current}`);
|
||||
if (stat.isSymbolicLink())
|
||||
throw new Error(`Refusing symlink in auto-sync clone root path: ${current}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -83,7 +83,10 @@ export async function runAutoSyncOnce(
|
|||
const groupsToSync = new Set<string>();
|
||||
const result: AutoSyncRunResult = { synced: 0, analyzed: 0, skippedAnalysis: 0, failed: 0 };
|
||||
const commitInfoEntries: ProjectCommitInfoEntry[] = [];
|
||||
const actualConcurrency = resolveActualConcurrency(config.maxConcurrency, deps.getAvailableMemoryGB());
|
||||
const actualConcurrency = resolveActualConcurrency(
|
||||
config.maxConcurrency,
|
||||
deps.getAvailableMemoryGB(),
|
||||
);
|
||||
logger.info(
|
||||
`[auto-sync] Starting sync loop with max_concurrency=${actualConcurrency} analyze_failure_threshold=${config.analyzeFailureThreshold}.`,
|
||||
);
|
||||
|
|
@ -182,7 +185,9 @@ export async function runAutoSyncOnce(
|
|||
lastSyncTime,
|
||||
};
|
||||
} catch (err: unknown) {
|
||||
logger.error(`[auto-sync] Repository sync failed for ${item.remoteUrl}: ${(err as Error).message}`);
|
||||
logger.error(
|
||||
`[auto-sync] Repository sync failed for ${item.remoteUrl}: ${(err as Error).message}`,
|
||||
);
|
||||
return {
|
||||
kind: 'failed' as const,
|
||||
project: item.project,
|
||||
|
|
@ -258,7 +263,9 @@ export async function runAutoSyncOnce(
|
|||
groupMembershipOk = true;
|
||||
} catch (err: unknown) {
|
||||
result.failed += 1;
|
||||
logger.error(`[auto-sync] Group update failed for ${repoResult.project.groupName}: ${(err as Error).message}`);
|
||||
logger.error(
|
||||
`[auto-sync] Group update failed for ${repoResult.project.groupName}: ${(err as Error).message}`,
|
||||
);
|
||||
}
|
||||
if (groupMembershipOk && repoResult.analyzeStatus === 'success') {
|
||||
groupsToSync.add(repoResult.project.groupName);
|
||||
|
|
@ -321,7 +328,10 @@ export function resolveActualConcurrency(configured: number, availableMemoryGB:
|
|||
return Math.max(1, Math.min(configured, memoryLimit));
|
||||
}
|
||||
|
||||
async function buildWorkItems(config: AutoSyncConfig, deps: AutoSyncRunDeps): Promise<AutoSyncWorkItem[]> {
|
||||
async function buildWorkItems(
|
||||
config: AutoSyncConfig,
|
||||
deps: AutoSyncRunDeps,
|
||||
): Promise<AutoSyncWorkItem[]> {
|
||||
const items: AutoSyncWorkItem[] = [];
|
||||
const targetOwners = new Map<string, string>();
|
||||
for (const project of config.projects) {
|
||||
|
|
@ -332,7 +342,9 @@ async function buildWorkItems(config: AutoSyncConfig, deps: AutoSyncRunDeps): Pr
|
|||
const targetDir = getConfiguredRepoPath({ localPath: cloneRoot.root }, repoName);
|
||||
const previous = targetOwners.get(targetDir);
|
||||
if (previous !== undefined) {
|
||||
throw new Error(`Duplicate auto-sync targetDir ${targetDir} for ${previous} and ${remoteUrl}`);
|
||||
throw new Error(
|
||||
`Duplicate auto-sync targetDir ${targetDir} for ${previous} and ${remoteUrl}`,
|
||||
);
|
||||
}
|
||||
targetOwners.set(targetDir, remoteUrl);
|
||||
} catch (err: unknown) {
|
||||
|
|
|
|||
|
|
@ -48,15 +48,17 @@ export function getAutoSyncWatchPaths(gitnexusDir = getGlobalDir()): AutoSyncWat
|
|||
};
|
||||
}
|
||||
|
||||
export async function startAutoSyncWatch(options: {
|
||||
setIntervalFn?: typeof setInterval;
|
||||
clearIntervalFn?: typeof clearInterval;
|
||||
runOnce?: typeof runAutoSyncOnce;
|
||||
stderr?: Pick<NodeJS.WriteStream, 'write'>;
|
||||
keepAlive?: boolean;
|
||||
paths?: AutoSyncWatchPaths;
|
||||
deps?: Partial<AutoSyncWatchControlDeps>;
|
||||
} = {}): Promise<AutoSyncStartHandle | null> {
|
||||
export async function startAutoSyncWatch(
|
||||
options: {
|
||||
setIntervalFn?: typeof setInterval;
|
||||
clearIntervalFn?: typeof clearInterval;
|
||||
runOnce?: typeof runAutoSyncOnce;
|
||||
stderr?: Pick<NodeJS.WriteStream, 'write'>;
|
||||
keepAlive?: boolean;
|
||||
paths?: AutoSyncWatchPaths;
|
||||
deps?: Partial<AutoSyncWatchControlDeps>;
|
||||
} = {},
|
||||
): Promise<AutoSyncStartHandle | null> {
|
||||
const stderr = options.stderr ?? process.stderr;
|
||||
const paths = options.paths ?? getAutoSyncWatchPaths();
|
||||
const deps = resolveWatchDeps(options.deps);
|
||||
|
|
@ -199,13 +201,15 @@ async function acquireWatchLock(
|
|||
}
|
||||
}
|
||||
|
||||
export async function stopAutoSyncWatch(options: {
|
||||
paths?: AutoSyncWatchPaths;
|
||||
stderr?: Pick<NodeJS.WriteStream, 'write'>;
|
||||
deps?: Partial<AutoSyncWatchControlDeps>;
|
||||
timeoutMs?: number;
|
||||
pollMs?: number;
|
||||
} = {}): Promise<boolean> {
|
||||
export async function stopAutoSyncWatch(
|
||||
options: {
|
||||
paths?: AutoSyncWatchPaths;
|
||||
stderr?: Pick<NodeJS.WriteStream, 'write'>;
|
||||
deps?: Partial<AutoSyncWatchControlDeps>;
|
||||
timeoutMs?: number;
|
||||
pollMs?: number;
|
||||
} = {},
|
||||
): Promise<boolean> {
|
||||
const stderr = options.stderr ?? process.stderr;
|
||||
const paths = options.paths ?? getAutoSyncWatchPaths();
|
||||
const deps = resolveWatchDeps(options.deps);
|
||||
|
|
@ -428,14 +432,20 @@ async function readStatusFile(statusPath: string): Promise<WatchStatusRecord | u
|
|||
}
|
||||
}
|
||||
|
||||
async function writeWatchStatus(paths: AutoSyncWatchPaths, record: WatchStatusRecord): Promise<void> {
|
||||
async function writeWatchStatus(
|
||||
paths: AutoSyncWatchPaths,
|
||||
record: WatchStatusRecord,
|
||||
): Promise<void> {
|
||||
await fs.mkdir(path.dirname(paths.statusPath), { recursive: true });
|
||||
const tmpPath = `${paths.statusPath}.tmp.${process.pid}.${Date.now()}`;
|
||||
await fs.writeFile(tmpPath, `${JSON.stringify(record, null, 2)}\n`, 'utf-8');
|
||||
await fs.rename(tmpPath, paths.statusPath);
|
||||
}
|
||||
|
||||
async function cleanupWatchFiles(paths: AutoSyncWatchPaths, lockHandle?: fs.FileHandle): Promise<void> {
|
||||
async function cleanupWatchFiles(
|
||||
paths: AutoSyncWatchPaths,
|
||||
lockHandle?: fs.FileHandle,
|
||||
): Promise<void> {
|
||||
await lockHandle?.close().catch(() => {});
|
||||
await removeIfExists(paths.pidPath);
|
||||
await removeIfExists(paths.lockPath);
|
||||
|
|
|
|||
|
|
@ -106,7 +106,12 @@ export interface ProjectCommitInfoEntry {
|
|||
branch?: string;
|
||||
codeCommitId?: string;
|
||||
analyzedCommitId?: string;
|
||||
status: AutoSyncAnalyzeStatus | 'sync_failed' | 'branch_skipped' | 'branch_unavailable' | 'sync_timeout';
|
||||
status:
|
||||
| AutoSyncAnalyzeStatus
|
||||
| 'sync_failed'
|
||||
| 'branch_skipped'
|
||||
| 'branch_unavailable'
|
||||
| 'sync_timeout';
|
||||
analyzeConsecutiveFailures?: number;
|
||||
analyzeFailureThreshold?: number;
|
||||
lastAnalyzeError?: string;
|
||||
|
|
|
|||
|
|
@ -790,11 +790,7 @@ export function buildGitEnv(
|
|||
// host-scoped Authorization header (GitHub PAT for github.com, else the
|
||||
// server's AZURE_DEVOPS_PAT for Azure hosts) via the GIT_CONFIG_* protocol —
|
||||
// never in argv. See resolveGitCredential / buildExtraHeaderKey.
|
||||
function runGit(
|
||||
args: string[],
|
||||
cwd?: string,
|
||||
options?: RunGitOptions,
|
||||
): Promise<void> {
|
||||
function runGit(args: string[], cwd?: string, options?: RunGitOptions): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const spawnGit = options?.spawnForTest ?? spawn;
|
||||
const proc = spawnGit('git', args, {
|
||||
|
|
|
|||
|
|
@ -13,7 +13,11 @@ import {
|
|||
startAutoSyncWatch,
|
||||
stopAutoSyncWatch,
|
||||
} from '../../src/core/auto-sync/index.js';
|
||||
import type { AutoSyncConfig, AutoSyncRunDeps, AutoSyncWatchPaths } from '../../src/core/auto-sync/index.js';
|
||||
import type {
|
||||
AutoSyncConfig,
|
||||
AutoSyncRunDeps,
|
||||
AutoSyncWatchPaths,
|
||||
} from '../../src/core/auto-sync/index.js';
|
||||
|
||||
const config: AutoSyncConfig = {
|
||||
configPath: '/tmp/.gitnexus/watch_config.yml',
|
||||
|
|
@ -44,7 +48,11 @@ function withCloneRoot(deps: Partial<AutoSyncRunDeps>): Partial<AutoSyncRunDeps>
|
|||
};
|
||||
}
|
||||
|
||||
async function writeWatchOwner(paths: AutoSyncWatchPaths, pid: number, ownerId = `owner-${pid}`): Promise<string> {
|
||||
async function writeWatchOwner(
|
||||
paths: AutoSyncWatchPaths,
|
||||
pid: number,
|
||||
ownerId = `owner-${pid}`,
|
||||
): Promise<string> {
|
||||
await fs.mkdir(path.dirname(paths.pidPath), { recursive: true });
|
||||
await fs.writeFile(paths.pidPath, `${pid}\n`);
|
||||
await fs.writeFile(
|
||||
|
|
@ -473,7 +481,9 @@ describe('auto-sync runner', () => {
|
|||
}),
|
||||
);
|
||||
expect(errorLogger).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Repository sync failed for git@gitee.com:qts_server/failing_sync.git'),
|
||||
expect.stringContaining(
|
||||
'Repository sync failed for git@gitee.com:qts_server/failing_sync.git',
|
||||
),
|
||||
);
|
||||
expect(errorLogger).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Analysis failed for /tmp/repos/qts_account'),
|
||||
|
|
@ -540,7 +550,9 @@ describe('auto-sync runner', () => {
|
|||
return remoteUrl.includes('/one.git') ? '/tmp/repos/one' : '/tmp/repos/two';
|
||||
}),
|
||||
getCurrentBranch: vi.fn(() => 'master'),
|
||||
getCurrentCommit: vi.fn((repoPath) => (repoPath.endsWith('/one') ? 'one-commit' : 'two-commit')),
|
||||
getCurrentCommit: vi.fn((repoPath) =>
|
||||
repoPath.endsWith('/one') ? 'one-commit' : 'two-commit',
|
||||
),
|
||||
runFullAnalysis: vi.fn(async () => ({ stats: { files: 1 } }) as any),
|
||||
registerRepo: vi.fn(async () => 'repo'),
|
||||
loadState: vi.fn(async () => ({})),
|
||||
|
|
@ -792,17 +804,10 @@ describe('auto-sync runner', () => {
|
|||
await fs.mkdir(groupDir, { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(groupDir, 'group.yaml'),
|
||||
[
|
||||
'version: 1',
|
||||
'name: back_end',
|
||||
'repos:',
|
||||
' hr/hiring/backend: qts_account',
|
||||
].join('\n'),
|
||||
['version: 1', 'name: back_end', 'repos:', ' hr/hiring/backend: qts_account'].join('\n'),
|
||||
);
|
||||
|
||||
await expect(addRepoToGroup({ groupName: 'back_end' }, 'qts_account')).resolves.toBe(
|
||||
false,
|
||||
);
|
||||
await expect(addRepoToGroup({ groupName: 'back_end' }, 'qts_account')).resolves.toBe(false);
|
||||
|
||||
await expect(fs.readFile(path.join(groupDir, 'group.yaml'), 'utf-8')).resolves.toContain(
|
||||
'hr/hiring/backend: qts_account',
|
||||
|
|
@ -854,7 +859,9 @@ describe('auto-sync starter', () => {
|
|||
expect(setIntervalFn).toHaveBeenCalledWith(expect.any(Function), 300_000);
|
||||
expect(timer.unref).toHaveBeenCalled();
|
||||
await vi.waitFor(() => {
|
||||
expect(stderr.write).toHaveBeenCalledWith(expect.stringContaining('[auto-sync] Watch loop started at '));
|
||||
expect(stderr.write).toHaveBeenCalledWith(
|
||||
expect.stringContaining('[auto-sync] Watch loop started at '),
|
||||
);
|
||||
expect(stderr.write).toHaveBeenCalledWith(
|
||||
'[auto-sync] Watch loop finished: synced=0 analyzed=0 skipped=0 failed=0.\n',
|
||||
);
|
||||
|
|
@ -935,7 +942,9 @@ describe('auto-sync starter', () => {
|
|||
});
|
||||
|
||||
expect(handle).toBeNull();
|
||||
expect(stderr.write).toHaveBeenCalledWith('[auto-sync] Watch is already running with pid 12345.\n');
|
||||
expect(stderr.write).toHaveBeenCalledWith(
|
||||
'[auto-sync] Watch is already running with pid 12345.\n',
|
||||
);
|
||||
} finally {
|
||||
await fs.rm(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
|
|
@ -1014,7 +1023,9 @@ describe('auto-sync starter', () => {
|
|||
});
|
||||
|
||||
expect(handle).toBeNull();
|
||||
expect(stderr.write).toHaveBeenCalledWith('[auto-sync] Watch is already running with pid 12345.\n');
|
||||
expect(stderr.write).toHaveBeenCalledWith(
|
||||
'[auto-sync] Watch is already running with pid 12345.\n',
|
||||
);
|
||||
expect(await fs.readFile(paths.lockPath, 'utf-8')).toContain('starting-owner');
|
||||
await expect(fs.access(paths.pidPath)).rejects.toThrow();
|
||||
} finally {
|
||||
|
|
@ -1073,7 +1084,11 @@ describe('auto-sync starter', () => {
|
|||
stopAutoSyncWatch({
|
||||
paths,
|
||||
stderr: { write: vi.fn() },
|
||||
deps: { isProcessAlive: vi.fn(() => false), killProcess: vi.fn(), sleep: vi.fn(async () => {}) },
|
||||
deps: {
|
||||
isProcessAlive: vi.fn(() => false),
|
||||
killProcess: vi.fn(),
|
||||
sleep: vi.fn(async () => {}),
|
||||
},
|
||||
}),
|
||||
).resolves.toBe(false);
|
||||
|
||||
|
|
@ -1146,7 +1161,9 @@ describe('auto-sync starter', () => {
|
|||
}),
|
||||
).resolves.toBe(false);
|
||||
|
||||
await expect(readAutoSyncWatchStatus(paths, { isProcessAlive: vi.fn(() => true) })).resolves.toMatchObject({
|
||||
await expect(
|
||||
readAutoSyncWatchStatus(paths, { isProcessAlive: vi.fn(() => true) }),
|
||||
).resolves.toMatchObject({
|
||||
state: 'stopping',
|
||||
pid: 12345,
|
||||
message: expect.stringContaining('did not exit'),
|
||||
|
|
@ -1182,7 +1199,9 @@ describe('auto-sync starter', () => {
|
|||
).resolves.toBe(false);
|
||||
|
||||
expect(killProcess).not.toHaveBeenCalled();
|
||||
await expect(readAutoSyncWatchStatus(paths, { isProcessAlive: vi.fn(() => true) })).resolves.toMatchObject({
|
||||
await expect(
|
||||
readAutoSyncWatchStatus(paths, { isProcessAlive: vi.fn(() => true) }),
|
||||
).resolves.toMatchObject({
|
||||
state: 'error',
|
||||
pid: 12345,
|
||||
message: expect.stringContaining('owner'),
|
||||
|
|
|
|||
|
|
@ -176,14 +176,14 @@ describe('auto-sync', () => {
|
|||
|
||||
it('hard-fails unsafe configured clone roots', async () => {
|
||||
await expect(resolveConfiguredCloneRoot('/')).rejects.toThrow('unsafe auto-sync clone root');
|
||||
await expect(resolveConfiguredCloneRoot(os.homedir())).rejects.toThrow('unsafe auto-sync clone root');
|
||||
await expect(resolveConfiguredCloneRoot(path.join(await fs.realpath(os.tmpdir()), 'repos'))).rejects.toThrow(
|
||||
await expect(resolveConfiguredCloneRoot(os.homedir())).rejects.toThrow(
|
||||
'unsafe auto-sync clone root',
|
||||
);
|
||||
await expect(
|
||||
resolveConfiguredCloneRoot(path.join(await fs.realpath(os.tmpdir()), 'repos')),
|
||||
).rejects.toThrow('unsafe auto-sync clone root');
|
||||
const root = path.join(tempDir, 'repos');
|
||||
await expect(resolveConfiguredCloneRoot(`${root}/../repos`)).rejects.toThrow(
|
||||
'normalized',
|
||||
);
|
||||
await expect(resolveConfiguredCloneRoot(`${root}/../repos`)).rejects.toThrow('normalized');
|
||||
});
|
||||
|
||||
it('rejects GitNexus internal directory descendants as clone roots', async () => {
|
||||
|
|
@ -258,7 +258,9 @@ describe('auto-sync', () => {
|
|||
expect(extractRepoNameFromRemoteUrl('git@gitee.com:qts_server/qts_account.git')).toBe(
|
||||
'qts_account',
|
||||
);
|
||||
expect(extractRepoNameFromRemoteUrl('git@gitlab.com:team/subgroup/repo-name.git')).toBe('repo-name');
|
||||
expect(extractRepoNameFromRemoteUrl('git@gitlab.com:team/subgroup/repo-name.git')).toBe(
|
||||
'repo-name',
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects unsafe repository names without sanitizing them', () => {
|
||||
|
|
@ -271,11 +273,19 @@ describe('auto-sync', () => {
|
|||
it('allows only github, gitlab, and gitee SSH SCP remote URLs', () => {
|
||||
expect(() => validateAutoSyncRemoteUrl('git@github.com:im-fan/multica.git')).not.toThrow();
|
||||
expect(() => validateAutoSyncRemoteUrl('git@gitlab.com:group/subgroup/repo.git')).not.toThrow();
|
||||
expect(() => validateAutoSyncRemoteUrl('git@gitee.com:qts-ops/qts-code-engineering.git')).not.toThrow();
|
||||
expect(() => validateAutoSyncRemoteUrl('https://github.com/owner/repo.git')).toThrow('must use');
|
||||
expect(() => validateAutoSyncRemoteUrl('ssh://git@github.com/owner/repo.git')).toThrow('must use');
|
||||
expect(() =>
|
||||
validateAutoSyncRemoteUrl('git@gitee.com:qts-ops/qts-code-engineering.git'),
|
||||
).not.toThrow();
|
||||
expect(() => validateAutoSyncRemoteUrl('https://github.com/owner/repo.git')).toThrow(
|
||||
'must use',
|
||||
);
|
||||
expect(() => validateAutoSyncRemoteUrl('ssh://git@github.com/owner/repo.git')).toThrow(
|
||||
'must use',
|
||||
);
|
||||
expect(() => validateAutoSyncRemoteUrl('user@github.com:owner/repo.git')).toThrow('must use');
|
||||
expect(() => validateAutoSyncRemoteUrl('git@example.com:owner/repo.git')).toThrow('host must be');
|
||||
expect(() => validateAutoSyncRemoteUrl('git@example.com:owner/repo.git')).toThrow(
|
||||
'host must be',
|
||||
);
|
||||
});
|
||||
|
||||
it('parses repo git timeout durations', () => {
|
||||
|
|
@ -318,9 +328,7 @@ describe('auto-sync', () => {
|
|||
previousStatus: 'failed',
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(shouldAnalyzeCommit({ currentCommit: 'def', previousAnalyzedCommit: 'abc' })).toBe(
|
||||
true,
|
||||
);
|
||||
expect(shouldAnalyzeCommit({ currentCommit: 'def', previousAnalyzedCommit: 'abc' })).toBe(true);
|
||||
});
|
||||
|
||||
it('saves state atomically and reloads it', async () => {
|
||||
|
|
@ -345,13 +353,13 @@ describe('auto-sync', () => {
|
|||
);
|
||||
await expect(loadAutoSyncState(statePath)).resolves.toEqual({
|
||||
'/tmp/repos/qts_account|master': {
|
||||
codeCommitId: 'abc',
|
||||
analyzedCommitId: 'abc',
|
||||
lastAnalyzeStatus: 'success',
|
||||
analyzeConsecutiveFailures: 2,
|
||||
lastAnalyzeError: 'old error',
|
||||
lastSyncTime: '2026-06-30T00:00:00.000Z',
|
||||
},
|
||||
codeCommitId: 'abc',
|
||||
analyzedCommitId: 'abc',
|
||||
lastAnalyzeStatus: 'success',
|
||||
analyzeConsecutiveFailures: 2,
|
||||
lastAnalyzeError: 'old error',
|
||||
lastSyncTime: '2026-06-30T00:00:00.000Z',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -672,7 +672,9 @@ describe('git-clone', () => {
|
|||
try {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const proc = spawn('git', ['init'], { cwd: root, stdio: 'ignore' });
|
||||
proc.on('close', (code) => (code === 0 ? resolve() : reject(new Error(`git init ${code}`))));
|
||||
proc.on('close', (code) =>
|
||||
code === 0 ? resolve() : reject(new Error(`git init ${code}`)),
|
||||
);
|
||||
proc.on('error', reject);
|
||||
});
|
||||
await fs.rename(path.join(root, '.git'), path.join(target, '.git')).catch(async () => {
|
||||
|
|
@ -719,9 +721,9 @@ describe('git-clone', () => {
|
|||
).rejects.toThrow('git clone failed');
|
||||
|
||||
const entries = await fs.readdir(quarantineRoot);
|
||||
expect(entries.some((entry) => entry.startsWith('auto-sync-') && entry.endsWith('-repo'))).toBe(
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
entries.some((entry) => entry.startsWith('auto-sync-') && entry.endsWith('-repo')),
|
||||
).toBe(true);
|
||||
} finally {
|
||||
await fs.rm(root, { recursive: true, force: true });
|
||||
}
|
||||
|
|
@ -881,9 +883,9 @@ describe('git-clone', () => {
|
|||
|
||||
describe('extractWebRepoName — API clone compatibility', () => {
|
||||
it('sanitizes repo names with spaces and unsafe directory characters at the web boundary', () => {
|
||||
expect(
|
||||
extractWebRepoName('https://dev.azure.com/org/project/_git/My Repo With Spaces'),
|
||||
).toBe('My_Repo_With_Spaces');
|
||||
expect(extractWebRepoName('https://dev.azure.com/org/project/_git/My Repo With Spaces')).toBe(
|
||||
'My_Repo_With_Spaces',
|
||||
);
|
||||
expect(extractWebRepoName('https://example.com/team/repo$name.git')).toBe('repo_name');
|
||||
});
|
||||
|
||||
|
|
@ -1102,9 +1104,11 @@ describe('git-clone', () => {
|
|||
|
||||
await vi.advanceTimersByTimeAsync(25);
|
||||
let settled = false;
|
||||
promise.catch(() => {}).finally(() => {
|
||||
settled = true;
|
||||
});
|
||||
promise
|
||||
.catch(() => {})
|
||||
.finally(() => {
|
||||
settled = true;
|
||||
});
|
||||
await vi.runAllTicks();
|
||||
expect(child.kill).toHaveBeenCalledWith('SIGTERM');
|
||||
expect(settled).toBe(false);
|
||||
|
|
|
|||
|
|
@ -411,14 +411,32 @@ describe('windowsHide regression', () => {
|
|||
/**
|
||||
* Count spawn-family invocations. The regex matches ``spawn(``,
|
||||
* ``spawnSync(``, ``execFile(``, ``execFileSync(``,
|
||||
* ``execFileAsync(``, ``execSync(`` as function calls — not
|
||||
* destructures (``const { spawn } = ...``), not method calls
|
||||
* (``.exec(``), not bare ``exec()`` (which collides with regex
|
||||
* ``.exec()``; we explicitly drop it).
|
||||
* ``execFileAsync(``, ``execSync(`` and simple local aliases that
|
||||
* point at one of those functions as function calls — not destructures
|
||||
* (``const { spawn } = ...``), not method calls (``.exec(``), not bare
|
||||
* ``exec()`` (which collides with regex ``.exec()``; we explicitly
|
||||
* drop it).
|
||||
*/
|
||||
function countSpawnCalls(codeSource: string): number {
|
||||
const re =
|
||||
/(^|[^a-zA-Z0-9_$.])(spawn|spawnSync|execFile|execFileSync|execFileAsync|execSync)\s*\(/gm;
|
||||
const spawnFunctions = [
|
||||
'spawn',
|
||||
'spawnSync',
|
||||
'execFile',
|
||||
'execFileSync',
|
||||
'execFileAsync',
|
||||
'execSync',
|
||||
];
|
||||
const spawnNames = new Set(spawnFunctions);
|
||||
const aliasRe = new RegExp(
|
||||
`\\bconst\\s+([A-Za-z_$][\\w$]*)\\s*=\\s*[^;\\n]*\\b(?:${spawnFunctions.join('|')})\\b`,
|
||||
'g',
|
||||
);
|
||||
let aliasMatch: RegExpExecArray | null;
|
||||
while ((aliasMatch = aliasRe.exec(codeSource)) !== null) {
|
||||
spawnNames.add(aliasMatch[1]);
|
||||
}
|
||||
|
||||
const re = new RegExp(`(^|[^a-zA-Z0-9_$.])(${[...spawnNames].join('|')})\\s*\\(`, 'gm');
|
||||
let count = 0;
|
||||
while (re.exec(codeSource) !== null) {
|
||||
count++;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue