const fs = require('fs'); const path = require('path'); const crypto = require('crypto'); const { execFileSync } = require('child_process'); const { resolveCloneUrlForGit, stripGitCredentials, buildGitHttpAuthConfigArgs, shouldApplyDefaultGitAuth, } = require('./gitCloneUrl.cjs'); function gitEnv() { return { ...process.env, GIT_TERMINAL_PROMPT: '0', }; } function sha16(s) { return crypto.createHash('sha256').update(String(s)).digest('hex').slice(0, 16); } /** * 浅克隆学员仓库到 .cache/student-repos// ,已存在则 git pull。 * @param {string} gitUrl 完整 https 地址(私有库可在 URL 中带 token) * @param {string} rootDir 项目根 * @returns {string} 本地克隆目录绝对路径 */ function ensureStudentRepo(gitUrl, rootDir) { const publicUrl = String(gitUrl).trim(); const authCfg = buildGitHttpAuthConfigArgs(publicUrl); /** 使用 extraHeader 时不要用带密码的 origin URL(避免被凭证助手改写后仍无法拉取) */ const originUrlForRemote = shouldApplyDefaultGitAuth(publicUrl) ? stripGitCredentials(publicUrl) : resolveCloneUrlForGit(publicUrl); const cacheRoot = path.join(rootDir, '.cache', 'student-repos'); fs.mkdirSync(cacheRoot, { recursive: true }); const dir = path.join(cacheRoot, sha16(stripGitCredentials(publicUrl))); if (fs.existsSync(path.join(dir, '.git'))) { try { execFileSync('git', ['-C', dir, 'remote', 'set-url', 'origin', originUrlForRemote], { stdio: 'pipe', env: gitEnv(), }); execFileSync( 'git', [...authCfg, '-C', dir, 'pull', '--ff-only'], { stdio: 'pipe', timeout: 300000, env: gitEnv() }, ); } catch { fs.rmSync(dir, { recursive: true, force: true }); execFileSync( 'git', [...authCfg, 'clone', '--depth', '1', originUrlForRemote, dir], { stdio: 'pipe', timeout: 300000, env: gitEnv() }, ); } } else { execFileSync( 'git', [...authCfg, 'clone', '--depth', '1', originUrlForRemote, dir], { stdio: 'pipe', timeout: 300000, env: gitEnv() }, ); } if (!fs.existsSync(path.join(dir, '.git'))) { throw new Error('git clone 后仍未发现 .git,请检查仓库地址与网络'); } return dir; } module.exports = { ensureStudentRepo, sha16 };