feat(worktree-ensure): 定位工作包的工作樹,不在就重建

處理 PR 留言的人不必先手動 cd 到正確的目錄:路徑由 owner/repo/分支名 純函式推導,
問這一支就知道該在哪裡動手。

「不在就重建」是常態不是防禦性程式設計。進度完全不寫在本機——換一台機器或換一個
agent 接手時,工作樹本來就不存在,而重建的成本就是一次 git worktree add。

兩種情況明確中止而不是硬幹:分支在本機與遠端都不見時報 BRANCH_NOT_FOUND(憑空長一棵
空的工作樹只會讓人以為進度還在),推導出的路徑上是別的東西時報 WORKTREE_PATH_TAKEN。

議題 #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 7196385c5e
commit 2562555058
2 changed files with 261 additions and 0 deletions
+65
View File
@@ -0,0 +1,65 @@
#!/usr/bin/env node
/**
* 定位一顆工作包的工作樹,不在就重建。
*
* 處理 PR 留言的人不必先手動 `cd` 到正確的目錄:路徑由 `owner/repo/分支名` 純函式推導
* (見 lib 的 `worktreePath`),問這一支就知道該在哪裡動手。
*
* **「不在就重建」是常態,不是防禦性程式設計。** 進度完全不寫在本機——換一台機器或
* 換一個 agent 接手時,工作樹本來就不存在,而重建的成本就是一次 `git worktree add`。
*
* 重建與建立走同一套(lib 的 `planWorktree`/`createWorktree`):一樣先 `git fetch`
* 更新遠端引用,一樣不設 upstream,分支已經存在就接上去而不是長一棵空的。兩邊各寫
* 一份,遲早會在「起點取自哪裡」這種地方分岔,而那種分岔要等到有人的進度不見了
* 才會被發現。
*
* 分支在本機與遠端都不存在時明確中止:憑空長一棵空的工作樹,只會讓人以為進度還在。
* 推導出的路徑上是別的東西時也中止,不盲目拿來用。
*
* 用法:
* node scripts/worktree-ensure.js --repo owner/name --branch <分支名>
* [--path <目標專案>] [--dry-run]
*/
import {
ScriptError,
createWorktree,
main,
openGitRepo,
parseFlags,
parseRepo,
planWorktree,
worktreePath,
} from './lib.js';
main(async () => {
const flags = parseFlags(process.argv.slice(2), {
required: ['repo', 'branch'],
optional: ['path'],
booleans: ['dry-run'],
});
const repo = parseRepo(flags.repo);
const branch = flags.branch;
const path = flags.path ?? process.cwd();
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`,
);
}
// 不給 source:這裡只重建既有分支的工作樹,沒有「從來源長一支新的」那條路,
// 那是 branch-prep 的事——在這裡憑空開一支新分支,等於把 PR 的進度扔掉
const plan = planWorktree(git, { branch, worktree });
const 報告 = { path, repo, branch, worktree, 動作: plan.動作 };
if (flags['dry-run']) {
return { dryRun: true, ...報告, commands: plan.commands.map((args) => `git ${args.join(' ')}`) };
}
createWorktree(git, plan, { worktree, branch });
return { ...報告, 重建: plan.commands.length > 0 };
});