fix(lib): 補回搬家時掉了的 rmSync,並讓 origin 檢查只有一份

review 抓到一個沉默的失效:rollback 最後那一手「git 清不乾淨時把目錄本身也清掉」呼叫
rmSync,但把這段從 branch-prep 搬進 lib 時,import 留在了原處。那一行落在自己的
try/catch 裡,所以 ReferenceError 被吞掉——註解承諾的事從來沒發生過,而且測試全綠。
下一次重跑會撞上 WORKTREE_PATH_TAKEN,人看到的是一句與真正原因無關的錯誤。

順手收掉兩支腳本各寫一次的 origin 檢查(只有「為什麼需要它」那一句不同,由呼叫端給),
並把 lib 檔頭「負責六件事」改成七件——工作樹的一生現在整個住在這裡。

正本三處跟著改:
- 查現況那一行補上 --dry-run。pr-watch 在 PR 已終止時會順手清掉工作樹,而「我想看一下
  留言」不該把清理順便做掉——清不清理是使用者的決定。
- 拿掉「使用者堅持要繼續就繼續」:它與同一份正本的邊界(已合併或關閉時不繼續處理留言)
  直接矛盾,而在一棵該被清掉的工作樹上改東西,那些改動不會進到任何 PR 裡。
- worktree-ensure 的指令補上 --path:目標專案多半不是當前目錄,而「不必先手動 cd」
  正是這一步要解決的問題。

議題 #42

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-17 17:33:44 +08:00
co-authored by Claude Opus 5
parent 0dd8d7b7bb
commit 66064a751e
6 changed files with 63 additions and 24 deletions
+2 -6
View File
@@ -51,6 +51,7 @@ import {
parseFlags,
parseRepo,
planWorktree,
requireOrigin,
worktreePath,
} from './lib.js';
@@ -91,12 +92,7 @@ main(async () => {
const worktree = worktreePath(repo, branch);
const git = openGitRepo(path);
if (!git('remote').split('\n').includes('origin')) {
throw new ScriptError(
'NO_ORIGIN',
`${path} 沒有 origin 遠端;工作樹的起點一律取自 origin/{來源分支},請先設定 origin`,
);
}
requireOrigin(git, path, '工作樹的起點一律取自 origin/{來源分支}');
const plan = planWorktree(git, { source, branch, worktree });
const 報告 = { path, repo, source, branch, worktree, 分支: { 動作: plan.動作 } };
+26 -5
View File
@@ -1,19 +1,28 @@
/**
* tea-sdlc 所有腳本的共用地基。
*
* 這一層負責六件事,其餘腳本只寫自己的業務:
* 這一層負責七件事,其餘腳本只寫自己的業務:
* 1. 具名 flag 解析與單行 JSON 輸出({ok, data, error:{code, message}})
* 2. Gitea API 呼叫 —— 全專案唯一的 HTTP 出口
* 3. git 執行 —— 全專案唯一的子行程出口
* 4. 四層前置檢查
* 5. 冪等查重
* 6. 兩支抽取腳本共用的議題讀取
* 4. 工作樹的一生 —— 路徑推導、建立(含重建)、現況、移除
* 5. 四層前置檢查
* 6. 冪等查重
* 7. 兩支抽取腳本共用的議題讀取
*
* 外部相依集中在 giteaRequest 與 runGit 兩個函式,測試才有地方替身。
*/
import { execFileSync } from 'node:child_process';
import { createHash } from 'node:crypto';
import { accessSync, constants, existsSync, readFileSync, realpathSync, statSync } from 'node:fs';
import {
accessSync,
constants,
existsSync,
readFileSync,
realpathSync,
rmSync,
statSync,
} from 'node:fs';
import { homedir } from 'node:os';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
@@ -430,6 +439,18 @@ export function runGit(args, { cwd } = {}) {
}
}
/**
* 要求目標專案有 origin 遠端。
* 工作樹的起點一律取自遠端,沒有 origin 就什麼都做不了——兩支腳本擋的是同一件事,
* 只有「為什麼需要它」那一句不同,所以那一句由呼叫端給。
* @param {string} 用途 出現在訊息裡的理由,例如「工作樹的起點一律取自 origin/{來源分支}」
*/
export function requireOrigin(git, path, 用途) {
if (!git('remote').split('\n').includes('origin')) {
throw new ScriptError('NO_ORIGIN', `${path} 沒有 origin 遠端;${用途},請先設定 origin`);
}
}
/**
* 開一個目標專案的 git repo,回傳綁在它身上的執行器。
*
+2 -7
View File
@@ -21,13 +21,13 @@
* [--path <目標專案>] [--dry-run]
*/
import {
ScriptError,
createWorktree,
main,
openGitRepo,
parseFlags,
parseRepo,
planWorktree,
requireOrigin,
worktreePath,
} from './lib.js';
@@ -43,12 +43,7 @@ main(async () => {
const worktree = worktreePath(repo, branch);
const git = openGitRepo(path);
if (!git('remote').split('\n').includes('origin')) {
throw new ScriptError(
'NO_ORIGIN',
`${path} 沒有 origin 遠端;重建工作樹要先能讀到 origin/${branch},請先設定 origin`,
);
}
requireOrigin(git, path, `重建工作樹要先能讀到 origin/${branch}`);
// 不給 source:這裡只重建既有分支的工作樹,沒有「從來源長一支新的」那條路,
// 那是 branch-prep 的事——在這裡憑空開一支新分支,等於把 PR 的進度扔掉