test(commit-split): 改名時別漏掉舊檔的刪除,失敗時說出做到哪裡

git diff --name-only 預設偵測改名,只印出目的地那一個路徑。來源的刪除因此被漏掉——
留在 index 裡沒被提交,而腳本還回報成功,要等下一次跑才會發現工作區不乾淨。加 --no-renames。

某一批提交失敗時,錯誤現在會列出前面已經建立的那幾顆 commit。不回捲它們:那會動到使用者的
歷史,而那幾顆本身是好的;但一定要說出做到哪裡,否則重跑前得自己去翻 git log。

類型對照表補上目標專案常見的測試擺法:tests/、spec/、__tests__/,以及放在被測檔案旁邊的
user.test.js。先前只認 test/,目標專案的測試會被併進 feat 那一批。
This commit is contained in:
2026-09-17 08:23:24 +00:00
parent f99adab245
commit 8a6145ea14
+40
View File
@@ -42,6 +42,11 @@ const subjects = (repo) =>
const CLASSIFY = [ const CLASSIFY = [
{ path: 'test/claim.test.js', type: 'test', why: '測試檔' }, { path: 'test/claim.test.js', type: 'test', why: '測試檔' },
{ path: 'test/helpers/stub-gitea.js', type: 'test', why: '測試用的 helper 也算測試' }, { path: 'test/helpers/stub-gitea.js', type: 'test', why: '測試用的 helper 也算測試' },
{ path: 'tests/user_test.py', type: 'test', why: '目標專案未必叫 test/' },
{ path: 'spec/user_spec.rb', type: 'test', why: '同上' },
{ path: '__tests__/user.js', type: 'test', why: '同上' },
{ path: 'src/user.test.js', type: 'test', why: '測試與程式碼放在一起也很常見' },
{ path: 'src/User.spec.ts', type: 'test', why: '同上' },
{ path: 'README.md', type: 'docs', why: '根目錄的說明文件' }, { path: 'README.md', type: 'docs', why: '根目錄的說明文件' },
{ path: 'AGENTS.md', type: 'docs', why: '同上' }, { path: 'AGENTS.md', type: 'docs', why: '同上' },
{ path: 'package.json', type: 'chore', why: '專案設定' }, { path: 'package.json', type: 'chore', why: '專案設定' },
@@ -306,6 +311,41 @@ test('被刪掉的檔案也照樣分類、照樣進 commit', async (t) => {
assert.equal(repo.git('status', '--porcelain'), ''); assert.equal(repo.git('status', '--porcelain'), '');
}); });
test('改名時舊檔的刪除也要進 commit,不能只提交新檔', async (t) => {
// git diff --name-only 預設偵測改名,只印目的地那一個路徑。漏掉來源等於把刪除留在
// index 裡,而腳本還回報成功——下一次跑才會發現工作區不乾淨。
const repo = withRepo(t);
write(repo, 'scripts/old.js');
repo.git('add', '-A');
repo.git('commit', '-qm', '先有這個檔案');
repo.git('mv', 'scripts/old.js', 'scripts/new.js');
const { code, json } = await run(repo, ['--type', 'refactor', '--scope', '改名', '--subject', '換個名字']);
assert.equal(code, 0, JSON.stringify(json));
assert.deepEqual(json.data.commits[0].files, ['scripts/new.js', 'scripts/old.js']);
assert.equal(repo.git('status', '--porcelain'), '', '改名的兩邊都要進同一顆 commit');
});
// ── 中途失敗 ───────────────────────────────────────────────────────
test('某一批提交失敗時,錯誤要說出前面已經建立了哪幾顆 commit', async (t) => {
// 沒說的話,使用者不知道做到哪裡,重跑前得自己去翻 git log
const repo = withRepo(t);
write(repo, 'scripts/one.js', 'test/one.test.js');
// 用 pre-commit hook 擋掉測試那一批
const hook = join(repo.dir, '.git', 'hooks', 'pre-commit');
mkdirSync(dirname(hook), { recursive: true });
writeFileSync(hook, '#!/bin/sh\ngit diff --cached --name-only | grep -q "^test/" && exit 1\nexit 0\n', { mode: 0o755 });
const { code, json } = await run(repo, ['--type', 'feat', '--subject', '做一件事']);
assert.equal(code, 1);
assert.equal(json.error.code, 'COMMIT_FAILED');
assert.match(json.error.message, /feat\(one\): 做一件事/, '要指名已經建立的那一顆');
assert.match(json.error.message, /test\(one\)/, '也要指名是哪一批失敗的');
});
// ── --dry-run ───────────────────────────────────────────────────── // ── --dry-run ─────────────────────────────────────────────────────
test('--dry-run 印出將建立的 commit 與各自的檔案,但不提交', async (t) => { test('--dry-run 印出將建立的 commit 與各自的檔案,但不提交', async (t) => {