fix(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:
+30
-11
@@ -20,9 +20,8 @@
|
|||||||
* [--scope <功能名>] [--body '<為什麼>'] [--files a.js,b.js]
|
* [--scope <功能名>] [--body '<為什麼>'] [--files a.js,b.js]
|
||||||
* [--path <目標專案>] [--dry-run]
|
* [--path <目標專案>] [--dry-run]
|
||||||
*/
|
*/
|
||||||
import { existsSync } from 'node:fs';
|
import { basename } from 'node:path';
|
||||||
import { basename, join } from 'node:path';
|
import { ScriptError, main, openGitRepo, parseFlags } from './lib.js';
|
||||||
import { ScriptError, main, parseFlags, runGit } from './lib.js';
|
|
||||||
|
|
||||||
/** commit 訊息的類型。與既有 git 歷史一致,不另立新詞。 */
|
/** commit 訊息的類型。與既有 git 歷史一致,不另立新詞。 */
|
||||||
const TYPES = ['feat', 'fix', 'refactor', 'test', 'docs', 'chore', 'perf', 'style'];
|
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`。
|
* 都是產品本身,是新增還是修正得由做的人說,所以不在這張表裡——它們吃 `--type`。
|
||||||
*/
|
*/
|
||||||
const BY_PATH = [
|
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: 'docs', match: (path) => /^[^/]+\.md$/.test(path) || path.startsWith('docs/') },
|
||||||
{
|
{
|
||||||
type: 'chore',
|
type: 'chore',
|
||||||
@@ -57,11 +62,7 @@ main(async () => {
|
|||||||
const type = parseType(flags.type);
|
const type = parseType(flags.type);
|
||||||
const subject = parseSubject(flags.subject);
|
const subject = parseSubject(flags.subject);
|
||||||
|
|
||||||
if (!existsSync(join(path, '.git'))) {
|
const git = openGitRepo(path);
|
||||||
throw new ScriptError('NOT_A_GIT_REPO', `${path} 不是 git repo;請用 --path 指向目標專案的根目錄`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const git = (...args) => runGit(args, { cwd: path });
|
|
||||||
const { changed, untracked } = changedFiles(git);
|
const { changed, untracked } = changedFiles(git);
|
||||||
if (changed.length === 0) {
|
if (changed.length === 0) {
|
||||||
throw new ScriptError('NOTHING_TO_COMMIT', `${path} 的工作區是乾淨的,沒有東西可以提交`);
|
throw new ScriptError('NOTHING_TO_COMMIT', `${path} 的工作區是乾淨的,沒有東西可以提交`);
|
||||||
@@ -73,13 +74,28 @@ main(async () => {
|
|||||||
return { dryRun: true, path, commits };
|
return { dryRun: true, path, commits };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const done = [];
|
||||||
for (const { message, files } of commits) {
|
for (const { message, files } of commits) {
|
||||||
|
try {
|
||||||
// 只有未追蹤的檔案需要先 add:commit 帶 pathspec 不會把新檔案收進來,
|
// 只有未追蹤的檔案需要先 add:commit 帶 pathspec 不會把新檔案收進來,
|
||||||
// 但已追蹤的修改與刪除它自己處理得了。對已經被 git rm 掉的檔案再 add 一次只會報
|
// 但已追蹤的修改與刪除它自己處理得了。對已經被 git rm 掉的檔案再 add 一次只會報
|
||||||
// 「找不到這個路徑」——那個檔案本來就已經不在工作區也不在 index 裡了。
|
// 「找不到這個路徑」——那個檔案本來就已經不在工作區也不在 index 裡了。
|
||||||
const toAdd = files.filter((file) => untracked.has(file));
|
const toAdd = files.filter((file) => untracked.has(file));
|
||||||
if (toAdd.length > 0) git('add', '--', ...toAdd);
|
if (toAdd.length > 0) git('add', '--', ...toAdd);
|
||||||
git('commit', '-m', message, '--', ...files);
|
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 })) };
|
return { path, commits: commits.map(({ message, files }) => ({ message, files })) };
|
||||||
});
|
});
|
||||||
@@ -96,8 +112,11 @@ main(async () => {
|
|||||||
* @returns {{changed: string[], untracked: Set<string>}}
|
* @returns {{changed: string[], untracked: Set<string>}}
|
||||||
*/
|
*/
|
||||||
function changedFiles(git) {
|
function changedFiles(git) {
|
||||||
// 已追蹤的改動:staged 與未 staged 都算,刪除與改名(列為一刪一增)也在內
|
// --no-renames 是必要的:git 預設偵測改名,只印出目的地那一個路徑,
|
||||||
const tracked = git('diff', '--name-only', 'HEAD').split('\n').filter((file) => file !== '');
|
// 來源的刪除就會被漏掉——留在 index 裡沒被提交,而腳本還回報成功
|
||||||
|
const tracked = git('diff', '--name-only', '--no-renames', 'HEAD')
|
||||||
|
.split('\n')
|
||||||
|
.filter((file) => file !== '');
|
||||||
const untracked = git('ls-files', '--others', '--exclude-standard')
|
const untracked = git('ls-files', '--others', '--exclude-standard')
|
||||||
.split('\n')
|
.split('\n')
|
||||||
.filter((file) => file !== '');
|
.filter((file) => file !== '');
|
||||||
|
|||||||
Reference in New Issue
Block a user