/** * 在 .tmp/ 底下開一個真的 git repo。 * git 操作不做 mock:在臨時 repo 上跑真的 git 比假的 git 可信,成本也低。 */ import { execFileSync } from 'node:child_process'; import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'; import { join } from 'node:path'; import { tmpRoot } from './run-script.js'; /** * @returns {{dir: string, git: Function, cleanup: Function}} dir 為已有一顆 commit 的 git repo, * git 為綁在它身上的執行器(與 makeTempRepoWithRemote 對稱) */ export function makeTempRepo() { mkdirSync(tmpRoot, { recursive: true }); const dir = mkdtempSync(join(tmpRoot, 'repo-')); const git = gitIn(dir); git('init', '-q', '-b', 'master'); seed(git, dir, 'tester', 'tester@example.com'); return { dir, git, cleanup: () => rmSync(dir, { recursive: true, force: true }) }; } /** * 綁在某個目錄上的 git 執行器。 * 全域與系統設定一律切掉:測試的結果不該隨開發者機器上的 git 設定而變。 */ function gitIn(cwd) { return (...args) => execFileSync('git', args, { cwd, encoding: 'utf8', env: { ...process.env, GIT_CONFIG_GLOBAL: '/dev/null', GIT_CONFIG_SYSTEM: '/dev/null' }, }).trim(); } /** 設好身分並放一顆起始 commit,讓 repo 有東西可以長分支 */ function seed(git, dir, name, email) { git('config', 'user.name', name); git('config', 'user.email', email); writeFileSync(join(dir, 'README.md'), '# temp\n'); git('add', '-A'); git('commit', '-qm', 'initial'); } /** * 開一個「有遠端」的臨時 repo:一個 bare 當 origin,一個工作用的 clone。 * * branch-prep 的重點行為都繞著遠端打轉——來源分支在遠端已存在時要 pull 而不是重建、 * 目標分支已存在時不能覆蓋他人進度。這些事沒有遠端就驗不出來。 * git 不做 mock:在臨時 repo 上跑真的 git 比假的 git 可信,成本也低。 * * @returns {{dir: string, origin: string, git: Function, pushFromElsewhere: Function, cleanup: Function}} * dir 為工作用 clone,origin 為 bare repo,pushFromElsewhere 用來模擬別人推東西上去 */ export function makeTempRepoWithRemote() { mkdirSync(tmpRoot, { recursive: true }); const base = mkdtempSync(join(tmpRoot, 'remote-')); const origin = join(base, 'origin.git'); const dir = join(base, 'work'); execFileSync('git', ['init', '-q', '--bare', '-b', 'master', origin]); execFileSync('git', ['clone', '-q', origin, dir]); const git = gitIn(dir); seed(git, dir, 'tester', 'tester@example.com'); git('push', '-q', 'origin', 'master'); return { dir, origin, git, /** 模擬「別人推了東西上去」:在 origin 上直接造一顆 commit */ pushFromElsewhere: (branch, filename, content) => { const other = join(base, `other-${filename}`); execFileSync('git', ['clone', '-q', origin, other]); const otherGit = gitIn(other); otherGit('config', 'user.name', 'someone-else'); otherGit('config', 'user.email', 'other@example.com'); otherGit('checkout', '-q', '-B', branch); writeFileSync(join(other, filename), content); otherGit('add', '-A'); otherGit('commit', '-qm', `別人的進度:${filename}`); otherGit('push', '-q', 'origin', branch); rmSync(other, { recursive: true, force: true }); }, cleanup: () => rmSync(base, { recursive: true, force: true }), }; }