🐛 fix(vscode): worktreePath 消费侧统一解析 ~/绝对/相对,修复带 ~ 的存量工单与绝对路径 worktree 误判
新增 resolveWorktreePath(stored, workspaceRoot) 三态合一解析:开头 ~ 展开为 home、 已是绝对原样、相对拼到 workspaceRoot。替换全部消费点,原先「判 isAbsolute 再 join」 不认 ~、issueLoader 算 worktreeExists 更是裸 join 连绝对路径都拼坏。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,21 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { execFile } from 'node:child_process'
|
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/<hash> 方案)。
|
||||||
|
*/
|
||||||
|
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 {
|
export interface WorktreeOpts {
|
||||||
/** Absolute path to the main workspace root. */
|
/** Absolute path to the main workspace root. */
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
|
|
||||||
import * as fs from 'node:fs'
|
import * as fs from 'node:fs'
|
||||||
import * as path from 'node:path'
|
import * as path from 'node:path'
|
||||||
|
import { resolveWorktreePath } from '../git/worktree'
|
||||||
import type { GiteaComment, GiteaIssue } from './api'
|
import type { GiteaComment, GiteaIssue } from './api'
|
||||||
import type { Issue, IssueColumn } from './types'
|
import type { Issue, IssueColumn } from './types'
|
||||||
import {
|
import {
|
||||||
@@ -354,7 +355,7 @@ async function buildIssue(opts: {
|
|||||||
let worktreeExists: boolean | undefined
|
let worktreeExists: boolean | undefined
|
||||||
if (worktreePath && workspaceRoot) {
|
if (worktreePath && workspaceRoot) {
|
||||||
try {
|
try {
|
||||||
worktreeExists = fs.existsSync(path.join(workspaceRoot, worktreePath))
|
worktreeExists = fs.existsSync(resolveWorktreePath(worktreePath, workspaceRoot))
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
worktreeExists = false
|
worktreeExists = false
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import * as fs from 'node:fs'
|
|||||||
import { promises as fsp } from 'node:fs'
|
import { promises as fsp } from 'node:fs'
|
||||||
import * as os from 'node:os'
|
import * as os from 'node:os'
|
||||||
import * as path from 'node:path'
|
import * as path from 'node:path'
|
||||||
|
import { resolveWorktreePath } from '../../git/worktree'
|
||||||
import { commands, env, ThemeColor, Uri, window, workspace } from 'vscode'
|
import { commands, env, ThemeColor, Uri, window, workspace } from 'vscode'
|
||||||
import { getToken } from '../../auth/secrets'
|
import { getToken } from '../../auth/secrets'
|
||||||
import { getBrainstormPrompt } from '../../cc/prompts'
|
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.
|
// after the worktree (and its projects dir) are gone.
|
||||||
if ((implementSessionId || testSessionId) && worktreePath) {
|
if ((implementSessionId || testSessionId) && worktreePath) {
|
||||||
try {
|
try {
|
||||||
const worktreeAbs = path.isAbsolute(worktreePath)
|
const worktreeAbs = resolveWorktreePath(worktreePath, workspaceRoot)
|
||||||
? worktreePath
|
|
||||||
: path.join(workspaceRoot, worktreePath)
|
|
||||||
const srcProjectsDir = projectsDirFor(worktreeAbs)
|
const srcProjectsDir = projectsDirFor(worktreeAbs)
|
||||||
const dstProjectsDir = projectsDirFor(workspaceRoot)
|
const dstProjectsDir = projectsDirFor(workspaceRoot)
|
||||||
const sessionIds = [implementSessionId, testSessionId].filter(
|
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
|
// 5. Best-effort cleanup of the worktree. Failures are non-fatal — the
|
||||||
// state JSON already records done, user can manually clean later.
|
// state JSON already records done, user can manually clean later.
|
||||||
if (worktreePath) {
|
if (worktreePath) {
|
||||||
const abs = path.isAbsolute(worktreePath)
|
const abs = resolveWorktreePath(worktreePath, workspaceRoot)
|
||||||
? worktreePath
|
|
||||||
: path.join(workspaceRoot, worktreePath)
|
|
||||||
if (fs.existsSync(abs)) {
|
if (fs.existsSync(abs)) {
|
||||||
// worktree 还被占用时(实施/测试会话终端 cwd 在里面、cc/codex 持有 git
|
// worktree 还被占用时(实施/测试会话终端 cwd 在里面、cc/codex 持有 git
|
||||||
// 锁或打开文件)git worktree remove 会失败。先 dispose 该工单的全部会话
|
// 锁或打开文件)git worktree remove 会失败。先 dispose 该工单的全部会话
|
||||||
@@ -960,9 +957,7 @@ export async function handleDeleteIssue(panel: KanbanWebviewPanel, issueNumber:
|
|||||||
|
|
||||||
// 5. 删 worktree(如有)— git worktree remove --force,失败立即停
|
// 5. 删 worktree(如有)— git worktree remove --force,失败立即停
|
||||||
if (worktreePath) {
|
if (worktreePath) {
|
||||||
const absWorktree = path.isAbsolute(worktreePath)
|
const absWorktree = resolveWorktreePath(worktreePath, workspaceRoot)
|
||||||
? worktreePath
|
|
||||||
: path.join(workspaceRoot, worktreePath)
|
|
||||||
if (fs.existsSync(absWorktree)) {
|
if (fs.existsSync(absWorktree)) {
|
||||||
// Pre-remove hook before nuking the worktree. Best-effort —
|
// Pre-remove hook before nuking the worktree. Best-effort —
|
||||||
// hook failure does NOT block the destructive delete.
|
// hook failure does NOT block the destructive delete.
|
||||||
@@ -1951,9 +1946,7 @@ export async function handleGeneratePrDiffSummary(panel: KanbanWebviewPanel, iss
|
|||||||
void window.showErrorMessage(`#${issueNumber} 尚未记录 worktree 路径`)
|
void window.showErrorMessage(`#${issueNumber} 尚未记录 worktree 路径`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const worktreePath = path.isAbsolute(stateWorktreePath)
|
const worktreePath = resolveWorktreePath(stateWorktreePath, workspaceRoot)
|
||||||
? stateWorktreePath
|
|
||||||
: path.join(workspaceRoot, stateWorktreePath)
|
|
||||||
let worktreeStat: fs.Stats
|
let worktreeStat: fs.Stats
|
||||||
try {
|
try {
|
||||||
worktreeStat = await fsp.stat(worktreePath)
|
worktreeStat = await fsp.stat(worktreePath)
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { watchForNewCodexSession } from '../../cc/codexSessionWatcher'
|
|||||||
import { getBrainstormContinuePrompt, getImplementPlanPrompt } from '../../cc/prompts'
|
import { getBrainstormContinuePrompt, getImplementPlanPrompt } from '../../cc/prompts'
|
||||||
import { projectsDirFor, watchForNewSession } from '../../cc/sessionWatcher'
|
import { projectsDirFor, watchForNewSession } from '../../cc/sessionWatcher'
|
||||||
import { detectRepo } from '../../git/remote'
|
import { detectRepo } from '../../git/remote'
|
||||||
import { createWorktree } from '../../git/worktree'
|
import { createWorktree, resolveWorktreePath } from '../../git/worktree'
|
||||||
import { logger } from '../../logging/logger'
|
import { logger } from '../../logging/logger'
|
||||||
import { getSettings } from '../../settings/store'
|
import { getSettings } from '../../settings/store'
|
||||||
import { webhookCoordinator } from '../../webhook/coordinator'
|
import { webhookCoordinator } from '../../webhook/coordinator'
|
||||||
@@ -257,7 +257,7 @@ export async function handleResumeReviewSession(panel: KanbanWebviewPanel, sessi
|
|||||||
void window.showErrorMessage(`审查会话无法恢复 #${issueNumber}:worktree 路径未记录`)
|
void window.showErrorMessage(`审查会话无法恢复 #${issueNumber}:worktree 路径未记录`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const worktreeAbs = path.join(workspaceRoot, relCwd)
|
const worktreeAbs = resolveWorktreePath(relCwd, workspaceRoot)
|
||||||
if (!fs.existsSync(worktreeAbs)) {
|
if (!fs.existsSync(worktreeAbs)) {
|
||||||
void window.showErrorMessage(`审查会话无法恢复 #${issueNumber}:worktree 不存在 ${worktreeAbs}`)
|
void window.showErrorMessage(`审查会话无法恢复 #${issueNumber}:worktree 不存在 ${worktreeAbs}`)
|
||||||
return
|
return
|
||||||
@@ -335,9 +335,7 @@ export async function triggerAutoReviewTab(panel: KanbanWebviewPanel, opts: {
|
|||||||
let effectiveCwd = opts.workspaceRoot
|
let effectiveCwd = opts.workspaceRoot
|
||||||
let cwdFallback = false
|
let cwdFallback = false
|
||||||
if (opts.worktreePath) {
|
if (opts.worktreePath) {
|
||||||
const abs = path.isAbsolute(opts.worktreePath)
|
const abs = resolveWorktreePath(opts.worktreePath, opts.workspaceRoot)
|
||||||
? opts.worktreePath
|
|
||||||
: path.join(opts.workspaceRoot, opts.worktreePath)
|
|
||||||
if (fs.existsSync(abs))
|
if (fs.existsSync(abs))
|
||||||
effectiveCwd = abs
|
effectiveCwd = abs
|
||||||
else
|
else
|
||||||
@@ -1400,10 +1398,8 @@ export async function startConflictResolution(panel: KanbanWebviewPanel, opts: {
|
|||||||
const settings = getSettings(panel.context)
|
const settings = getSettings(panel.context)
|
||||||
const devBranch = settings.devBranch || 'main'
|
const devBranch = settings.devBranch || 'main'
|
||||||
|
|
||||||
// worktreePath 在 state JSON 里通常是 workspace-relative;解析为绝对路径。
|
// worktreePath 可能是相对、绝对或带 ~ 的存量值;统一解析为绝对路径。
|
||||||
const worktreeAbs = path.isAbsolute(worktreePath)
|
const worktreeAbs = resolveWorktreePath(worktreePath, workspaceRoot)
|
||||||
? worktreePath
|
|
||||||
: path.join(workspaceRoot, worktreePath)
|
|
||||||
|
|
||||||
// worktree 是实施阶段建好的,按理一直存在;被清理过算异常,拒绝继续。
|
// worktree 是实施阶段建好的,按理一直存在;被清理过算异常,拒绝继续。
|
||||||
if (!fs.existsSync(worktreeAbs)) {
|
if (!fs.existsSync(worktreeAbs)) {
|
||||||
@@ -1626,7 +1622,7 @@ export function resolveTestProfilePath(_panel: KanbanWebviewPanel, testProfilePa
|
|||||||
*/
|
*/
|
||||||
async function resolveTestSessionCwd(workspaceRoot: string, worktreePathRel?: string): Promise<string> {
|
async function resolveTestSessionCwd(workspaceRoot: string, worktreePathRel?: string): Promise<string> {
|
||||||
if (worktreePathRel) {
|
if (worktreePathRel) {
|
||||||
const abs = path.join(workspaceRoot, worktreePathRel)
|
const abs = resolveWorktreePath(worktreePathRel, workspaceRoot)
|
||||||
try {
|
try {
|
||||||
await fsp.stat(abs)
|
await fsp.stat(abs)
|
||||||
return abs
|
return abs
|
||||||
|
|||||||
Reference in New Issue
Block a user