From 5e7a3d20d9a2a9319e72371eec9bc4a6fc6121e4 Mon Sep 17 00:00:00 2001 From: cruldra Date: Wed, 24 Jun 2026 03:01:08 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix(vscode):=20worktreePath=20?= =?UTF-8?q?=E6=B6=88=E8=B4=B9=E4=BE=A7=E7=BB=9F=E4=B8=80=E8=A7=A3=E6=9E=90?= =?UTF-8?q?=20~/=E7=BB=9D=E5=AF=B9/=E7=9B=B8=E5=AF=B9=EF=BC=8C=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E5=B8=A6=20~=20=E7=9A=84=E5=AD=98=E9=87=8F=E5=B7=A5?= =?UTF-8?q?=E5=8D=95=E4=B8=8E=E7=BB=9D=E5=AF=B9=E8=B7=AF=E5=BE=84=20worktr?= =?UTF-8?q?ee=20=E8=AF=AF=E5=88=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 resolveWorktreePath(stored, workspaceRoot) 三态合一解析:开头 ~ 展开为 home、 已是绝对原样、相对拼到 workspaceRoot。替换全部消费点,原先「判 isAbsolute 再 join」 不认 ~、issueLoader 算 worktreeExists 更是裸 join 连绝对路径都拼坏。 Co-Authored-By: Claude Opus 4.7 (1M context) --- vscode/src/git/worktree.ts | 15 +++++++++++++++ vscode/src/gitea/issueLoader.ts | 3 ++- vscode/src/panel/handlers/issues.ts | 17 +++++------------ vscode/src/panel/handlers/sessions.ts | 16 ++++++---------- 4 files changed, 28 insertions(+), 23 deletions(-) diff --git a/vscode/src/git/worktree.ts b/vscode/src/git/worktree.ts index 1bcef24..76b04e0 100644 --- a/vscode/src/git/worktree.ts +++ b/vscode/src/git/worktree.ts @@ -7,6 +7,21 @@ */ import { execFile } from 'node:child_process' +import * as os from 'node:os' +import * as path from 'node:path' + +/** + * 把存储的 worktreePath 解析成可用的绝对路径。三态合一: + * 开头 `~` → 展开为 home(git/execFile 不走 shell,自己不展开 `~`, + * 存量工单里可能存了带 `~` 的字面量);已是绝对 → 原样; + * 相对 → 拼到 workspaceRoot 下(旧的 .claude/worktrees/ 方案)。 + */ +export function resolveWorktreePath(stored: string, workspaceRoot: string): string { + let p = stored + if (p.startsWith('~')) + p = path.join(os.homedir(), p.slice(1)) + return path.isAbsolute(p) ? p : path.join(workspaceRoot, p) +} export interface WorktreeOpts { /** Absolute path to the main workspace root. */ diff --git a/vscode/src/gitea/issueLoader.ts b/vscode/src/gitea/issueLoader.ts index a5112eb..cf2e16a 100644 --- a/vscode/src/gitea/issueLoader.ts +++ b/vscode/src/gitea/issueLoader.ts @@ -19,6 +19,7 @@ import * as fs from 'node:fs' import * as path from 'node:path' +import { resolveWorktreePath } from '../git/worktree' import type { GiteaComment, GiteaIssue } from './api' import type { Issue, IssueColumn } from './types' import { @@ -354,7 +355,7 @@ async function buildIssue(opts: { let worktreeExists: boolean | undefined if (worktreePath && workspaceRoot) { try { - worktreeExists = fs.existsSync(path.join(workspaceRoot, worktreePath)) + worktreeExists = fs.existsSync(resolveWorktreePath(worktreePath, workspaceRoot)) } catch { worktreeExists = false diff --git a/vscode/src/panel/handlers/issues.ts b/vscode/src/panel/handlers/issues.ts index 504c4d3..7c9eed1 100644 --- a/vscode/src/panel/handlers/issues.ts +++ b/vscode/src/panel/handlers/issues.ts @@ -7,6 +7,7 @@ import * as fs from 'node:fs' import { promises as fsp } from 'node:fs' import * as os from 'node:os' import * as path from 'node:path' +import { resolveWorktreePath } from '../../git/worktree' import { commands, env, ThemeColor, Uri, window, workspace } from 'vscode' import { getToken } from '../../auth/secrets' import { getBrainstormPrompt } from '../../cc/prompts' @@ -406,9 +407,7 @@ export async function handleColumnChange(panel: KanbanWebviewPanel, issueNumber: // after the worktree (and its projects dir) are gone. if ((implementSessionId || testSessionId) && worktreePath) { try { - const worktreeAbs = path.isAbsolute(worktreePath) - ? worktreePath - : path.join(workspaceRoot, worktreePath) + const worktreeAbs = resolveWorktreePath(worktreePath, workspaceRoot) const srcProjectsDir = projectsDirFor(worktreeAbs) const dstProjectsDir = projectsDirFor(workspaceRoot) const sessionIds = [implementSessionId, testSessionId].filter( @@ -448,9 +447,7 @@ export async function handleColumnChange(panel: KanbanWebviewPanel, issueNumber: // 5. Best-effort cleanup of the worktree. Failures are non-fatal — the // state JSON already records done, user can manually clean later. if (worktreePath) { - const abs = path.isAbsolute(worktreePath) - ? worktreePath - : path.join(workspaceRoot, worktreePath) + const abs = resolveWorktreePath(worktreePath, workspaceRoot) if (fs.existsSync(abs)) { // worktree 还被占用时(实施/测试会话终端 cwd 在里面、cc/codex 持有 git // 锁或打开文件)git worktree remove 会失败。先 dispose 该工单的全部会话 @@ -960,9 +957,7 @@ export async function handleDeleteIssue(panel: KanbanWebviewPanel, issueNumber: // 5. 删 worktree(如有)— git worktree remove --force,失败立即停 if (worktreePath) { - const absWorktree = path.isAbsolute(worktreePath) - ? worktreePath - : path.join(workspaceRoot, worktreePath) + const absWorktree = resolveWorktreePath(worktreePath, workspaceRoot) if (fs.existsSync(absWorktree)) { // Pre-remove hook before nuking the worktree. Best-effort — // hook failure does NOT block the destructive delete. @@ -1951,9 +1946,7 @@ export async function handleGeneratePrDiffSummary(panel: KanbanWebviewPanel, iss void window.showErrorMessage(`#${issueNumber} 尚未记录 worktree 路径`) return } - const worktreePath = path.isAbsolute(stateWorktreePath) - ? stateWorktreePath - : path.join(workspaceRoot, stateWorktreePath) + const worktreePath = resolveWorktreePath(stateWorktreePath, workspaceRoot) let worktreeStat: fs.Stats try { worktreeStat = await fsp.stat(worktreePath) diff --git a/vscode/src/panel/handlers/sessions.ts b/vscode/src/panel/handlers/sessions.ts index aeb70ed..c4f2b0b 100644 --- a/vscode/src/panel/handlers/sessions.ts +++ b/vscode/src/panel/handlers/sessions.ts @@ -12,7 +12,7 @@ import { watchForNewCodexSession } from '../../cc/codexSessionWatcher' import { getBrainstormContinuePrompt, getImplementPlanPrompt } from '../../cc/prompts' import { projectsDirFor, watchForNewSession } from '../../cc/sessionWatcher' import { detectRepo } from '../../git/remote' -import { createWorktree } from '../../git/worktree' +import { createWorktree, resolveWorktreePath } from '../../git/worktree' import { logger } from '../../logging/logger' import { getSettings } from '../../settings/store' import { webhookCoordinator } from '../../webhook/coordinator' @@ -257,7 +257,7 @@ export async function handleResumeReviewSession(panel: KanbanWebviewPanel, sessi void window.showErrorMessage(`审查会话无法恢复 #${issueNumber}:worktree 路径未记录`) return } - const worktreeAbs = path.join(workspaceRoot, relCwd) + const worktreeAbs = resolveWorktreePath(relCwd, workspaceRoot) if (!fs.existsSync(worktreeAbs)) { void window.showErrorMessage(`审查会话无法恢复 #${issueNumber}:worktree 不存在 ${worktreeAbs}`) return @@ -335,9 +335,7 @@ export async function triggerAutoReviewTab(panel: KanbanWebviewPanel, opts: { let effectiveCwd = opts.workspaceRoot let cwdFallback = false if (opts.worktreePath) { - const abs = path.isAbsolute(opts.worktreePath) - ? opts.worktreePath - : path.join(opts.workspaceRoot, opts.worktreePath) + const abs = resolveWorktreePath(opts.worktreePath, opts.workspaceRoot) if (fs.existsSync(abs)) effectiveCwd = abs else @@ -1400,10 +1398,8 @@ export async function startConflictResolution(panel: KanbanWebviewPanel, opts: { const settings = getSettings(panel.context) const devBranch = settings.devBranch || 'main' - // worktreePath 在 state JSON 里通常是 workspace-relative;解析为绝对路径。 - const worktreeAbs = path.isAbsolute(worktreePath) - ? worktreePath - : path.join(workspaceRoot, worktreePath) + // worktreePath 可能是相对、绝对或带 ~ 的存量值;统一解析为绝对路径。 + const worktreeAbs = resolveWorktreePath(worktreePath, workspaceRoot) // worktree 是实施阶段建好的,按理一直存在;被清理过算异常,拒绝继续。 if (!fs.existsSync(worktreeAbs)) { @@ -1626,7 +1622,7 @@ export function resolveTestProfilePath(_panel: KanbanWebviewPanel, testProfilePa */ async function resolveTestSessionCwd(workspaceRoot: string, worktreePathRel?: string): Promise { if (worktreePathRel) { - const abs = path.join(workspaceRoot, worktreePathRel) + const abs = resolveWorktreePath(worktreePathRel, workspaceRoot) try { await fsp.stat(abs) return abs