✨ feat(vscode): 摘修复冲突不再中止,保留 worktree 自动开冲突解决会话接手

摘修复改在固定的发布 worktree(worktreeDirectory 模板,目录名为分支名
/ 换 -)里按序 cherry-pick -x。全部成功推远端并删 worktree;中途冲突则
保留冲突现场、远端不变,开 `<发布分支>-冲突解决` claude 会话解冲突、
--continue、按原顺序摘完剩余提交再推远端。同名终端活着直接注入提示词。

- 上次冲突没处理完再摘会被拒绝;目录模板解析到工作区本身也拒绝
- 目录被非 worktree 的垃圾占着时清掉重建
- 测试覆盖成功清理、冲突接力(含 -x 尾注保留)、拒绝工作区本身
- CLAUDE.md 补一条:收尾三步默认直接做,不问不提醒

Claude-Session: https://claude.ai/code/session_018HDxMmNDyfLa3bMTUctBzt
This commit is contained in:
cruldra committed 2026-09-06 13:32:47 +08:00
1 parent ee33ae2440
commit 706057075d
6 files changed
+186 -24

No files matched your search

+66 -2
View File
@@ -5,8 +5,11 @@
* 读设置、弹 QuickPick、发 toast、维护 `panel.releaseRunning` 互斥。
*/
import type { CherryPickConflict } from '../../git/release'
import type { KanbanWebviewPanel } from '../KanbanPanel'
import { window, workspace } from 'vscode'
import { buildCcCommand } from '../../cc/ccCommand'
import { resolveProfilePath } from '../../cc/profiles'
import { gitFetch } from '../../git/branches'
import {
attachPulls,
@@ -19,9 +22,12 @@ import {
listUnreleasedCommits,
todayReleaseBranch,
} from '../../git/release'
import { DEFAULT_WORKTREE_TEMPLATE, resolveWorktreeDir } from '../../git/worktreePath'
import { triggerJenkinsBuild } from '../../jenkins/trigger'
import { logger } from '../../logging/logger'
import { getSettings } from '../../settings/store'
import { dismiss, toast } from './handoffFlow'
import { submitToSessionTerminal } from './terminals'
export async function handleReleaseStatus(panel: KanbanWebviewPanel): Promise<void> {
const root = workspace.workspaceFolders?.[0]?.uri.fsPath
@@ -141,14 +147,72 @@ export function handleReleaseCherryPickConfirm(panel: KanbanWebviewPanel, shas:
const commits = await listUnreleasedCommits({ cwd: root, devBranch: s.devBranch, current })
const order = new Map(commits.map((c, i) => [c.sha, i]))
const ordered = [...shas].sort((a, b) => (order.get(b) ?? 0) - (order.get(a) ?? 0))
// 发布 worktree 走和实施 worktree 同一套目录模板,分支名里的 / 换成 - 当目录名。
const worktreePath = resolveWorktreeDir(s.worktreeDirectory || DEFAULT_WORKTREE_TEMPLATE, root, current.replace(/\//g, '-'))
const spinner = toast(panel, 'info', `正在摘 ${ordered.length} 个提交到 ${current}…`, { spinner: true, dismissOnTimer: 0 })
let conflict: CherryPickConflict | undefined
try {
await cherryPickToRelease({ cwd: root, branch: current, shas: ordered })
conflict = await cherryPickToRelease({ cwd: root, branch: current, shas: ordered, worktreePath })
}
finally {
dismiss(panel, spinner)
}
toast(panel, 'success', `已摘 ${ordered.length} 个提交到 ${current}`)
if (!conflict) {
toast(panel, 'success', `已摘 ${ordered.length} 个提交到 ${current}`)
return
}
openReleaseConflictSession(panel, { branch: current, conflict, subjectOf: new Map(commits.map(c => [c.sha, c.subject])) })
toast(panel, 'info', `${conflict.sha.slice(0, 7)} ${conflict.subject} 冲突,已开 ${current} 冲突解决会话接手`, { dismissOnTimer: 10_000 })
})
}
/**
* 摘修复冲突后的接力:worktree 里 cherry-pick 停在中间态,开一个 cc 会话进去解决、
* 按原顺序摘完剩余提交并推远端。同名终端还活着就把提示词直接打进去,不再新开。
*/
function openReleaseConflictSession(panel: KanbanWebviewPanel, o: { branch: string, conflict: CherryPickConflict, subjectOf: Map<string, string> }): void {
const { branch, conflict } = o
const remaining = conflict.remaining.length === 0
? ' (没有剩余提交)'
: conflict.remaining.map(sha => ` - ${sha} ${o.subjectOf.get(sha) ?? ''}`.trimEnd()).join('\n')
const prompt = `当前目录是发布分支 ${branch} 的 worktree(detached HEAD),执行 \`git cherry-pick -x ${conflict.sha}\`(${conflict.subject})时出现冲突,冲突已落地。你的任务:
1. 跑 \`git status\` 查看冲突文件
2. 逐个解决冲突,保留这个提交的改动意图
3. \`git add\` 已解决的文件,然后 \`git cherry-pick --continue\`(保留默认提交说明)
4. 按下面的顺序继续 \`git cherry-pick -x <sha>\`,再冲突就同样处理:
${remaining}
5. 全部完成后 \`git push origin HEAD:refs/heads/${branch}\`
注意:
- 不要 rebase、不要 force push、不要碰其他分支
- 全部推完后告诉用户「${branch} 已摘完并推送」`
const terminalName = `${branch}-冲突解决`
const live = panel.findExistingTerminal(terminalName)
if (live) {
live.show(false)
submitToSessionTerminal(live, prompt)
logger.add({ level: 'info', source: 'panel', message: `摘修复冲突:已把提示词打进 ${terminalName}` })
return
}
const profilePath = resolveProfilePath(getSettings(panel.context).conflictResolutionProfilePath.trim() || undefined)
if (profilePath.includes('\'')) {
toast(panel, 'error', `冲突解决失败:profilePath 含单引号 (${profilePath})`, { dismissOnTimer: 10_000 })
return
}
const terminal = window.createTerminal({
name: terminalName,
cwd: conflict.worktreePath,
location: panel.resolveTerminalLocation(false),
})
terminal.show(false)
// buildCcCommand 用单引号包提示词,内部的 ' 要转成 '\'' 才不会破壳。
terminal.sendText(buildCcCommand({ profilePath, effort: 'high', prompt: prompt.replace(/'/g, '\'\\\'\'') }))
logger.add({
level: 'info',
source: 'panel',
message: `摘修复冲突:cc 会话已启动 (${branch}, ${conflict.sha.slice(0, 7)}, cwd ${conflict.worktreePath}, profile ${profilePath})`,
})
}