一個 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 的修改會少掉檔名的第一個字元。
91 lines
3.4 KiB
JavaScript
91 lines
3.4 KiB
JavaScript
/**
|
||
* 在 .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 }),
|
||
};
|
||
}
|