Files
tea-sdlc/test/helpers/temp-repo.js
T
jiantw83 d6b44e0ba8 test(分批提交): 把變更依類型分批 commit
一個 commit 只裝一種類型:程式碼、測試、文件、雜項各自成批,reviewer 一次只看一件事,
日後 git log 也讀得懂。全部混成一顆「完成工作包」的巨大 commit,等於沒有歷史。

類型多半看得出來——測試檔就是 test、README 就是 docs——但 scripts/ 底下的改動是新功能
還是修 bug,只有做的人知道,所以那一批由 --type 指定。這張對照表是純字串規則,表格驅動。

scope 單檔用檔名(claim.test.js 的 scope 是 claim,不是 claim.test),多檔用 --scope 的
功能名。描述要有中文:日後回顧時看得懂的是中文,而夾雜英文的專有名詞本來就該保留原文。

--files 讓一次變更橫跨兩個功能時能分兩次跑;--body 讓工具產出的歷史與本 repo 既有的
commit 一樣說明得出「為什麼」。

列變更檔案刻意不用 git status --porcelain:它的前兩欄是狀態碼,而 runGit 會 trim 掉
輸出的前導空白,未 staged 的修改會少掉檔名的第一個字元。
2026-09-17 08:23:21 +00:00

91 lines
3.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 在 .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 }),
};
}