From f99adab2455ead61847217a02e6b47559aa8c18d Mon Sep 17 00:00:00 2001 From: Jeffery Date: Thu, 17 Sep 2026 08:23:23 +0000 Subject: [PATCH] =?UTF-8?q?fix(commit-split):=20=E6=94=B9=E5=90=8D?= =?UTF-8?q?=E6=99=82=E5=88=A5=E6=BC=8F=E6=8E=89=E8=88=8A=E6=AA=94=E7=9A=84?= =?UTF-8?q?=E5=88=AA=E9=99=A4=EF=BC=8C=E5=A4=B1=E6=95=97=E6=99=82=E8=AA=AA?= =?UTF-8?q?=E5=87=BA=E5=81=9A=E5=88=B0=E5=93=AA=E8=A3=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit git diff --name-only 預設偵測改名,只印出目的地那一個路徑。來源的刪除因此被漏掉—— 留在 index 裡沒被提交,而腳本還回報成功,要等下一次跑才會發現工作區不乾淨。加 --no-renames。 某一批提交失敗時,錯誤現在會列出前面已經建立的那幾顆 commit。不回捲它們:那會動到使用者的 歷史,而那幾顆本身是好的;但一定要說出做到哪裡,否則重跑前得自己去翻 git log。 類型對照表補上目標專案常見的測試擺法:tests/、spec/、__tests__/,以及放在被測檔案旁邊的 user.test.js。先前只認 test/,目標專案的測試會被併進 feat 那一批。 --- scripts/commit-split.js | 53 ++++++++++++++++++++++++++++------------- 1 file changed, 36 insertions(+), 17 deletions(-) diff --git a/scripts/commit-split.js b/scripts/commit-split.js index 7d1ad15..33e2884 100644 --- a/scripts/commit-split.js +++ b/scripts/commit-split.js @@ -20,9 +20,8 @@ * [--scope <功能名>] [--body '<為什麼>'] [--files a.js,b.js] * [--path <目標專案>] [--dry-run] */ -import { existsSync } from 'node:fs'; -import { basename, join } from 'node:path'; -import { ScriptError, main, parseFlags, runGit } from './lib.js'; +import { basename } from 'node:path'; +import { ScriptError, main, openGitRepo, parseFlags } from './lib.js'; /** commit 訊息的類型。與既有 git 歷史一致,不另立新詞。 */ const TYPES = ['feat', 'fix', 'refactor', 'test', 'docs', 'chore', 'perf', 'style']; @@ -34,7 +33,13 @@ const TYPES = ['feat', 'fix', 'refactor', 'test', 'docs', 'chore', 'perf', 'styl * 都是產品本身,是新增還是修正得由做的人說,所以不在這張表裡——它們吃 `--type`。 */ const BY_PATH = [ - { type: 'test', match: (path) => path.startsWith('test/') }, + { + // 目標專案的測試未必放在 test/:tests/、spec/、__tests__/ 都常見, + // 也常見把 user.test.js 放在被測檔案旁邊 + type: 'test', + match: (path) => + /(^|\/)(tests?|spec|__tests__)\//.test(path) || /\.(test|spec)\.[^./]+$/.test(path), + }, { type: 'docs', match: (path) => /^[^/]+\.md$/.test(path) || path.startsWith('docs/') }, { type: 'chore', @@ -57,11 +62,7 @@ main(async () => { const type = parseType(flags.type); const subject = parseSubject(flags.subject); - if (!existsSync(join(path, '.git'))) { - throw new ScriptError('NOT_A_GIT_REPO', `${path} 不是 git repo;請用 --path 指向目標專案的根目錄`); - } - - const git = (...args) => runGit(args, { cwd: path }); + const git = openGitRepo(path); const { changed, untracked } = changedFiles(git); if (changed.length === 0) { throw new ScriptError('NOTHING_TO_COMMIT', `${path} 的工作區是乾淨的,沒有東西可以提交`); @@ -73,13 +74,28 @@ main(async () => { return { dryRun: true, path, commits }; } + const done = []; for (const { message, files } of commits) { - // 只有未追蹤的檔案需要先 add:commit 帶 pathspec 不會把新檔案收進來, - // 但已追蹤的修改與刪除它自己處理得了。對已經被 git rm 掉的檔案再 add 一次只會報 - // 「找不到這個路徑」——那個檔案本來就已經不在工作區也不在 index 裡了。 - const toAdd = files.filter((file) => untracked.has(file)); - if (toAdd.length > 0) git('add', '--', ...toAdd); - git('commit', '-m', message, '--', ...files); + try { + // 只有未追蹤的檔案需要先 add:commit 帶 pathspec 不會把新檔案收進來, + // 但已追蹤的修改與刪除它自己處理得了。對已經被 git rm 掉的檔案再 add 一次只會報 + // 「找不到這個路徑」——那個檔案本來就已經不在工作區也不在 index 裡了。 + const toAdd = files.filter((file) => untracked.has(file)); + if (toAdd.length > 0) git('add', '--', ...toAdd); + git('commit', '-m', message, '--', ...files); + done.push(message); + } catch (cause) { + // 不回捲已經建立的 commit:那會動到使用者的歷史,而這幾顆本身是好的。 + // 但一定要說出做到哪裡,否則重跑前得自己去翻 git log。 + throw new ScriptError( + 'COMMIT_FAILED', + `這一批提交失敗:${message.split('\n')[0]}(${cause.message})。` + + (done.length > 0 + ? `在此之前已經建立:${done.map((m) => m.split('\n')[0]).join('、')};` + + '修掉原因之後重跑即可,已建立的那幾顆不會重複。' + : '還沒有任何 commit 被建立。'), + ); + } } return { path, commits: commits.map(({ message, files }) => ({ message, files })) }; }); @@ -96,8 +112,11 @@ main(async () => { * @returns {{changed: string[], untracked: Set}} */ function changedFiles(git) { - // 已追蹤的改動:staged 與未 staged 都算,刪除與改名(列為一刪一增)也在內 - const tracked = git('diff', '--name-only', 'HEAD').split('\n').filter((file) => file !== ''); + // --no-renames 是必要的:git 預設偵測改名,只印出目的地那一個路徑, + // 來源的刪除就會被漏掉——留在 index 裡沒被提交,而腳本還回報成功 + const tracked = git('diff', '--name-only', '--no-renames', 'HEAD') + .split('\n') + .filter((file) => file !== ''); const untracked = git('ls-files', '--others', '--exclude-standard') .split('\n') .filter((file) => file !== '');