117 lines
3.6 KiB
TypeScript
117 lines
3.6 KiB
TypeScript
import * as fs from 'node:fs';
|
|
import * as path from 'node:path';
|
|
import { execFileSync } from 'node:child_process';
|
|
import { createLogger } from '../../shared/utils/index.js';
|
|
import { loadProjectConfig } from '../config/index.js';
|
|
|
|
const log = createLogger('clone');
|
|
|
|
export function resolveCloneSubmoduleOptions(projectDir: string): { args: string[]; label: string; targets: string } {
|
|
const config = loadProjectConfig(projectDir);
|
|
const resolvedSubmodules = config.submodules ?? (config.withSubmodules === true ? 'all' : undefined);
|
|
|
|
if (resolvedSubmodules === 'all') {
|
|
return {
|
|
args: ['--recurse-submodules'],
|
|
label: 'with submodule',
|
|
targets: 'all',
|
|
};
|
|
}
|
|
|
|
if (Array.isArray(resolvedSubmodules) && resolvedSubmodules.length > 0) {
|
|
return {
|
|
args: resolvedSubmodules.map((submodulePath) => `--recurse-submodules=${submodulePath}`),
|
|
label: 'with submodule',
|
|
targets: resolvedSubmodules.join(', '),
|
|
};
|
|
}
|
|
|
|
return {
|
|
args: [],
|
|
label: 'without submodule',
|
|
targets: 'none',
|
|
};
|
|
}
|
|
|
|
function resolveMainRepo(projectDir: string): string {
|
|
const gitPath = path.join(projectDir, '.git');
|
|
|
|
try {
|
|
const stats = fs.statSync(gitPath);
|
|
if (stats.isFile()) {
|
|
const content = fs.readFileSync(gitPath, 'utf-8');
|
|
const match = content.match(/^gitdir:\s*(.+)$/m);
|
|
if (match && match[1]) {
|
|
const worktreePath = match[1].trim();
|
|
const gitDir = path.resolve(worktreePath, '..', '..');
|
|
const mainRepoPath = path.dirname(gitDir);
|
|
log.info('Detected worktree, using main repo', { worktree: projectDir, mainRepo: mainRepoPath });
|
|
return mainRepoPath;
|
|
}
|
|
}
|
|
} catch (err) {
|
|
log.debug('Failed to resolve main repo, using projectDir as-is', { error: String(err) });
|
|
}
|
|
|
|
return projectDir;
|
|
}
|
|
|
|
export function cloneAndIsolate(projectDir: string, clonePath: string, branch?: string): void {
|
|
const referenceRepo = resolveMainRepo(projectDir);
|
|
const cloneSubmoduleOptions = resolveCloneSubmoduleOptions(projectDir);
|
|
|
|
fs.mkdirSync(path.dirname(clonePath), { recursive: true });
|
|
|
|
const branchArgs = branch ? ['--branch', branch] : [];
|
|
const commonArgs: string[] = [
|
|
...cloneSubmoduleOptions.args,
|
|
...branchArgs,
|
|
projectDir,
|
|
clonePath,
|
|
];
|
|
|
|
const referenceCloneArgs = ['clone', '--reference', referenceRepo, '--dissociate', ...commonArgs];
|
|
const fallbackCloneArgs = ['clone', ...commonArgs];
|
|
|
|
try {
|
|
execFileSync('git', referenceCloneArgs, {
|
|
cwd: projectDir,
|
|
stdio: 'pipe',
|
|
});
|
|
} catch (err) {
|
|
const stderr = ((err as { stderr?: Buffer }).stderr ?? Buffer.alloc(0)).toString();
|
|
if (stderr.includes('reference repository is shallow')) {
|
|
log.info('Reference repository is shallow, retrying clone without --reference', { referenceRepo });
|
|
try { fs.rmSync(clonePath, { recursive: true, force: true }); } catch (e) { log.debug('Failed to cleanup partial clone before retry', { clonePath, error: String(e) }); }
|
|
execFileSync('git', fallbackCloneArgs, {
|
|
cwd: projectDir,
|
|
stdio: 'pipe',
|
|
});
|
|
} else {
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
execFileSync('git', ['remote', 'remove', 'origin'], {
|
|
cwd: clonePath,
|
|
stdio: 'pipe',
|
|
});
|
|
|
|
for (const key of ['user.name', 'user.email']) {
|
|
try {
|
|
const value = execFileSync('git', ['config', '--local', key], {
|
|
cwd: projectDir,
|
|
stdio: 'pipe',
|
|
}).toString().trim();
|
|
if (value) {
|
|
execFileSync('git', ['config', key, value], {
|
|
cwd: clonePath,
|
|
stdio: 'pipe',
|
|
});
|
|
}
|
|
} catch (err) {
|
|
log.debug('Local git config not found', { key, error: String(err) });
|
|
}
|
|
}
|
|
}
|