fix(claim): 被碼錶擋下時明說停錶不會動到既有的工作樹

議題 #38 的使用者故事第 30 條:使用者常以為停錶等於放棄那顆工作包,於是寧可
不停——工時就記到別顆議題去了。碼錶只管時間、工作樹只管檔案,兩者互不相干,
這件事要在擋下來的當下就講,不能指望使用者自己推論。

領取與起錶會撞到同一個擋路理由,訊息收進 lib 只寫一份。順手收掉 review 指出的
三處:planWorktree 沒用到的 repo 參數、與 path.resolve 同名而誤導的區域函式、
以及只有 lib 自己用得到卻對外 export 的兩支路徑函式。

回滾補上最後一道:git 清不掉時把目錄本身也刪掉。那條路徑在這次執行之前不存在
(不存在正是建立的前提),裡面不可能有使用者的東西,而留著它下一次重跑會直接
撞上 WORKTREE_PATH_TAKEN——一次失敗的建立不該讓人從此開不了工。

議題 #40

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-17 16:29:12 +08:00
co-authored by Claude Opus 5
parent 4583a5f210
commit 0154cf59d4
9 changed files with 65 additions and 36 deletions
+13 -6
View File
@@ -38,7 +38,7 @@
* node scripts/branch-prep.js --repo <owner/name> --source <來源分支> --slug <英文-kebab>
* [--type feat] [--path <目標專案>] [--dry-run]
*/
import { existsSync, realpathSync } from 'node:fs';
import { existsSync, realpathSync, rmSync } from 'node:fs';
import { join } from 'node:path';
import { ScriptError, main, openGitRepo, parseFlags, parseRepo, worktreePath } from './lib.js';
@@ -51,7 +51,7 @@ const FEATURE_BRANCH = /^([a-z]+)\/([^/]+)\/([^/]+)$/;
/**
* 專案檔 → 把依賴裝起來的指令。
* 工作樹是乾淨的,這份對照表只用來提示使用者該跑什麼,腳本自己不執行安裝——
* 在別人的機器上裝東西應該是他自己的決定。
* 在別人的機器上裝東西應該是他自己的決定。偵測不到就不提,猜錯的指令比沒有更浪費時間。
*/
const INSTALL_HINTS = [
['package.json', 'npm install'],
@@ -86,7 +86,7 @@ main(async () => {
);
}
const plan = planWorktree(git, { repo, source, branch, worktree });
const plan = planWorktree(git, { source, branch, worktree });
const 報告 = { path, repo, source, branch, worktree, 分支: { 動作: plan.動作 } };
if (flags['dry-run']) {
@@ -178,7 +178,7 @@ function isKebab(value) {
*
* @returns {{commands: string[][], 動作: string}} commands 為空代表工作樹已經在了
*/
function planWorktree(git, { repo, source, branch, worktree }) {
function planWorktree(git, { source, branch, worktree }) {
const 既有 = listWorktrees(git).find((entry) => samePath(entry.path, worktree));
const 目錄還在 = existsSync(worktree);
@@ -248,11 +248,11 @@ function listWorktrees(git) {
* (家目錄本身就常是一條連結),逐字比對會把同一棵工作樹判成兩棵。
*/
function samePath(a, b) {
return a === b || resolve(a) === resolve(b);
return a === b || realOrSelf(a) === realOrSelf(b);
}
/** 解析得出真實路徑就用它,路徑還不存在時退回原字串 */
function resolve(path) {
function realOrSelf(path) {
try {
return realpathSync(path);
} catch {
@@ -286,6 +286,13 @@ function rollback(git, { worktree, branch, 保留分支 }) {
quietly(git, ['worktree', 'remove', '--force', worktree]);
quietly(git, ['worktree', 'prune']);
if (!保留分支) quietly(git, ['branch', '-D', branch]);
// git 清不乾淨時把目錄本身也清掉:這條路徑在這次執行之前不存在(不存在是建立的前提),
// 裡面不可能有使用者的東西;留著它下一次重跑會直接撞上 WORKTREE_PATH_TAKEN
try {
rmSync(worktree, { recursive: true, force: true });
} catch {
// 連目錄都刪不掉就只能留著:原本的錯誤比清理的錯誤重要
}
}
/** 清理用的 git:失敗了也不能蓋掉真正的錯誤訊息,那才是使用者要看的東西。 */