review 抓到一處落差:試跑判「終止且工作樹乾淨」就預告 git worktree remove, 但實跑多判一件事——那條路徑上的東西是不是真的一棵工作樹。別的 clone 在同一條路徑上 留下目錄時(路徑由 owner/repo/分支名 推導,不含本機 clone 的位置),試跑會預告一行 實跑必然拒絕的指令,而回報裡完全看不出原因。 判斷收進 lib 的 inspectWorktree,由它直接給出 reason(missing/foreign/dirty/ removable):試跑與實跑、自動與手動四條路徑從此擋在同一個判斷上,worktree-remove 裡 那份重算的副本也跟著刪掉。回報多一個「是工作樹」欄位,手動出口的 NOT_A_WORKTREE 不再是使用者第一次聽到這件事。 順帶兩件同源的修正: - 「是不是工作樹」改認 .git 為**檔案**。獨立 clone 的 .git 是目錄,先前會被當成工作樹, 然後在 git worktree remove 那一步炸出一句原始錯誤。 - 兩支腳本的 --dry-run 請求預告收進 pr-threads 的 plannedRequests,並補上 pr-watch 先前漏掉的那句說明(逐則 reaction 與逐個 review 的行內留言事前列不完)。預告與實際 發出的請求分開寫,加一個端點就會有一邊忘了改。 - PR 讀不到 head 分支時給出 PULL_HEAD_MISSING,而不是拿空字串去推導一條路徑。 議題 #41 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
85 lines
2.9 KiB
JavaScript
85 lines
2.9 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* 手動清掉一棵工作樹。
|
|
*
|
|
* pr-watch 會在 PR 合併或關閉時自動清理,這一支是給那些**永遠不會被合併也不會被關閉**
|
|
* 的 PR 用的出口——沒有它,那些工作樹只能靠使用者自己記得去刪。
|
|
*
|
|
* 兩條路共用 lib 的 `removeWorktree`,不互相開子行程:守門的規則只有一份,
|
|
* 自動的那條與手動的這條不該長出兩種行為。
|
|
*
|
|
* 只移除工作樹,本機分支與遠端分支都留著。工作樹裡還有沒提交的東西就中止並報出路徑,
|
|
* **絕不 `--force`**:工作樹重建得回來,被刪掉的未提交變更救不回來。
|
|
*
|
|
* 路徑由 `owner/repo/分支名` 推導,所以輸入是這兩個而不是一條路徑——要刪哪一棵由
|
|
* 「哪顆工作包」決定,使用者不必自己去記 12 碼的雜湊目錄名。
|
|
*
|
|
* 用法:
|
|
* node scripts/worktree-remove.js --repo owner/name --branch <分支名> [--dry-run]
|
|
*/
|
|
import {
|
|
ScriptError,
|
|
inspectWorktree,
|
|
main,
|
|
parseFlags,
|
|
parseRepo,
|
|
removeWorktree,
|
|
worktreePath,
|
|
} from './lib.js';
|
|
|
|
main(async () => {
|
|
const flags = parseFlags(process.argv.slice(2), {
|
|
required: ['repo', 'branch'],
|
|
booleans: ['dry-run'],
|
|
});
|
|
const repo = parseRepo(flags.repo);
|
|
const branch = flags.branch;
|
|
const worktree = worktreePath(repo, branch);
|
|
|
|
// 試跑與實跑走同一條守門:試跑印得出漂亮的計畫、實跑卻被擋下來,是最難查的那種落差
|
|
if (flags['dry-run']) {
|
|
const state = inspectWorktree(worktree);
|
|
checkRemovable(state, worktree);
|
|
return {
|
|
dryRun: true,
|
|
repo,
|
|
branch,
|
|
worktree,
|
|
已經不在: state.reason === 'missing',
|
|
commands: state.reason === 'removable' ? [`git worktree remove ${worktree}`] : [],
|
|
};
|
|
}
|
|
|
|
const result = removeWorktree(worktree);
|
|
checkRemovable(result, worktree);
|
|
|
|
return {
|
|
repo,
|
|
branch,
|
|
worktree,
|
|
removed: result.removed,
|
|
// 本來就不在不算失敗:重跑這一支是常態,而結果一樣是「那棵樹不在了」
|
|
已經不在: result.reason === 'missing',
|
|
};
|
|
});
|
|
|
|
|
|
/** 擋下來的兩種情況各有各的下一步,錯誤碼要分得開 */
|
|
function checkRemovable(result, worktree) {
|
|
if (result.reason === 'dirty') {
|
|
throw new ScriptError(
|
|
'WORKTREE_DIRTY',
|
|
`工作樹 ${worktree} 裡還有沒提交的東西(${result.files.join('、')});` +
|
|
'請先提交、暫存(git stash)或確認可以丟掉再自己刪除——' +
|
|
'本工具不會加 --force,刪掉的未提交變更救不回來',
|
|
);
|
|
}
|
|
if (result.reason === 'foreign') {
|
|
throw new ScriptError(
|
|
'NOT_A_WORKTREE',
|
|
`${worktree} 上有東西,但它不是一棵 git 工作樹(可能是別的 clone 留下的);` +
|
|
'請自己確認裡面沒有還沒保存的東西之後移除它',
|
|
);
|
|
}
|
|
}
|