diff --git a/vscode/src/panel/handlers/branchCleanup.ts b/vscode/src/panel/handlers/branchCleanup.ts new file mode 100644 index 0000000..cd8b8ee --- /dev/null +++ b/vscode/src/panel/handlers/branchCleanup.ts @@ -0,0 +1,72 @@ +import { deleteLocalBranch } from '../../git/branchSync' +import { deleteBranch } from '../../gitea/api' +import { logger } from '../../logging/logger' + +/** + * 清理一个 feature 分支(远程 + 本地),供「拖到完成」和「手动删除 worktree」两处共用。 + * + * - 受保护分支(main/master/devBranch/autoBuildBranch)直接跳过,绝不删。 + * - 远程删除走 gitea API,需 remote + token;缺任一则只删本地,不阻断。 + * - 本地删除是强删(deleteLocalBranch 内部 `git branch -D`):调用方语义是「连分支一起删」, + * 未合并也照删。 + * - 全程 best-effort:远程 / 本地各自 try/catch,失败只记日志——分支清理从不回滚已完成的主流程 + * (worktree 已删、PR 已合并)。branch 为空直接返回。 + * + * 放在 handlers 层而非 `git/branchSync.ts`:后者刻意不依赖 `vscode`(有单测覆盖 + * deleteLocalBranch),而本函数要用 logger(→ vscode)和 gitea API。 + */ +export async function cleanupFeatureBranch(opts: { + workspaceRoot: string + branch: string + issueNumber: number + devBranch?: string + autoBuildBranch?: string + /** 缺 remote 或 token 时跳过远程删除,只删本地。 */ + remote?: { host: string, owner: string, repo: string } + token?: string +}): Promise { + const { workspaceRoot, branch, issueNumber, devBranch, autoBuildBranch, remote, token } = opts + if (!branch) + return + + const protectedBranches = new Set( + ['main', 'master', devBranch, autoBuildBranch].filter((b): b is string => Boolean(b)), + ) + if (protectedBranches.has(branch)) { + logger.add({ level: 'warn', source: 'panel', message: `跳过受保护分支清理 ${branch} (issue #${issueNumber})` }) + return + } + + if (remote && token) { + try { + await deleteBranch({ host: remote.host, token, owner: remote.owner, repo: remote.repo, branch }) + logger.add({ level: 'info', source: 'panel', message: `已删除远程 feature 分支 ${branch} (issue #${issueNumber})` }) + } + catch (err) { + logger.add({ + level: 'warn', + source: 'panel', + message: `删除远程 feature 分支失败 ${branch} (issue #${issueNumber})`, + details: err instanceof Error ? err.message : String(err), + }) + } + } + + try { + const deleted = await deleteLocalBranch(workspaceRoot, branch) + logger.add({ + level: deleted.ok ? 'info' : 'warn', + source: 'panel', + message: `${deleted.ok ? '已删除' : '删除失败'}本地 feature 分支 ${branch} (issue #${issueNumber})`, + details: deleted.stderr || deleted.stdout, + }) + } + catch (err) { + logger.add({ + level: 'warn', + source: 'panel', + message: `删除本地 feature 分支抛错 ${branch} (issue #${issueNumber})`, + details: err instanceof Error ? err.message : String(err), + }) + } +} diff --git a/vscode/src/panel/handlers/issues.ts b/vscode/src/panel/handlers/issues.ts index 1b8c813..1f9804b 100644 --- a/vscode/src/panel/handlers/issues.ts +++ b/vscode/src/panel/handlers/issues.ts @@ -13,7 +13,7 @@ import { buildCcCommand } from '../../cc/ccCommand' import { getBrainstormPrompt } from '../../cc/prompts' import { projectsDirFor, watchForNewSession } from '../../cc/sessionWatcher' import { spawnClaude } from '../../cc/spawnClaude' -import { deleteLocalBranch, gitFetch } from '../../git/branchSync' +import { gitFetch } from '../../git/branchSync' import { detectRepo } from '../../git/remote' import { addDependency, @@ -33,6 +33,7 @@ import { getSettings } from '../../settings/store' import { webhookCoordinator } from '../../webhook/coordinator' import { pickRandomIssueColor, themeColorIdToIconUri } from '../issueColor' import { DEFAULT_PROFILE_PATH, makeNonce, PR_DIFF_SUMMARY_PROFILE_PATH } from '../KanbanPanel' +import { cleanupFeatureBranch } from './branchCleanup' import * as sessions from './sessions' export async function handleColumnChange(panel: KanbanWebviewPanel, issueNumber: number, toColumn: IssueColumn): Promise { @@ -509,71 +510,15 @@ export async function handleColumnChange(panel: KanbanWebviewPanel, issueNumber: if (featureBranch) { const settingsForBranchCleanup = getSettings(panel.context) - const protectedBranches = new Set( - ['main', 'master', settingsForBranchCleanup.devBranch, settingsForBranchCleanup.autoBuildBranch] - .filter((branch): branch is string => Boolean(branch)), - ) - if (protectedBranches.has(featureBranch)) { - logger.add({ - level: 'warn', - source: 'panel', - message: `跳过受保护分支清理 ${featureBranch} (issue #${issueNumber})`, - }) - } - else { - try { - await deleteBranch({ - host: remote.host, - token, - owner: remote.owner, - repo: remote.repo, - branch: featureBranch, - }) - logger.add({ - level: 'info', - source: 'panel', - message: `已删除远程 feature 分支 ${featureBranch} (issue #${issueNumber})`, - }) - } - catch (err) { - const message = err instanceof Error ? err.message : String(err) - logger.add({ - level: 'warn', - source: 'panel', - message: `删除远程 feature 分支失败 ${featureBranch} (issue #${issueNumber})`, - details: message, - }) - } - - try { - const deleted = await deleteLocalBranch(workspaceRoot, featureBranch) - if (deleted.ok) { - logger.add({ - level: 'info', - source: 'panel', - message: `已删除本地 feature 分支 ${featureBranch} (issue #${issueNumber})`, - details: deleted.stdout || deleted.stderr, - }) - } - else { - logger.add({ - level: 'warn', - source: 'panel', - message: `删除本地 feature 分支失败 ${featureBranch} (issue #${issueNumber})`, - details: deleted.stderr || deleted.stdout, - }) - } - } - catch (err) { - const message = err instanceof Error ? err.message : String(err) - logger.add({ - level: 'warn', - source: 'panel', - message: `删除本地 feature 分支抛错 ${featureBranch} (issue #${issueNumber})`, - details: message, - }) - } - } + await cleanupFeatureBranch({ + workspaceRoot, + branch: featureBranch, + issueNumber, + devBranch: settingsForBranchCleanup.devBranch, + autoBuildBranch: settingsForBranchCleanup.autoBuildBranch, + remote, + token, + }) } // 6. 全部成功 → 增量推 done + 清掉 worktreePath + 标记 prMerged。 diff --git a/vscode/src/panel/handlers/worktree.ts b/vscode/src/panel/handlers/worktree.ts index 2ced5f3..679075f 100644 --- a/vscode/src/panel/handlers/worktree.ts +++ b/vscode/src/panel/handlers/worktree.ts @@ -14,6 +14,7 @@ import { import { logger } from '../../logging/logger' import { getSettings } from '../../settings/store' import { makeNonce } from '../KanbanPanel' +import { cleanupFeatureBranch } from './branchCleanup' /** * Open the worktree directory in a **new** VS Code window. The Boolean @@ -59,8 +60,10 @@ export async function handleOpenWorktree(panel: KanbanWebviewPanel, relPath: str } /** - * Confirm + 递归强删 worktree 目录(removeWorktreeDir),然后清掉 state JSON 里的 - * `worktreePath`/`branch` 并刷新看板。删除失败(如权限、护栏拦截)把错误弹给用户。 + * Confirm + 递归强删 worktree 目录(removeWorktreeDir)→ 清掉 state JSON 里的 + * `worktreePath`/`branch` → 连同该工单的 feature 分支一起删(本地 + 远程,best-effort) + * → 刷新看板。目录删除失败(权限 / 护栏拦截)把错误弹给用户;分支清理失败只记日志、 + * 不阻断——与「拖到完成」的清理行为一致。 */ export async function handleDeleteWorktree(panel: KanbanWebviewPanel, issueNumber: number, relPath: string): Promise { const workspaceRoot = workspace.workspaceFolders?.[0]?.uri.fsPath @@ -68,8 +71,11 @@ export async function handleDeleteWorktree(panel: KanbanWebviewPanel, issueNumbe void window.showErrorMessage('请先打开一个工作区文件夹') return } + // 先读分支名:确认框要提示「连分支一起删」,删完目录后也据此清分支。 + const state = await panel.readIssueState(issueNumber) + const branch = typeof state.branch === 'string' ? state.branch : '' const choice = await window.showWarningMessage( - `确认删除 worktree ${relPath}?`, + branch ? `确认删除 worktree ${relPath} 及其分支 ${branch}?` : `确认删除 worktree ${relPath}?`, { modal: true }, '删除', ) @@ -85,7 +91,7 @@ export async function handleDeleteWorktree(panel: KanbanWebviewPanel, issueNumbe await dispatchWorktreeHook(panel, 'pre-remove', { workspaceRoot, worktreePath: abs, - branch: '', + branch, issueNumber, mainBranch: settingsForHook.devBranch || 'main', customScriptPath: settingsForHook.worktreePreRemoveScript, @@ -110,6 +116,20 @@ export async function handleDeleteWorktree(panel: KanbanWebviewPanel, issueNumbe console.warn('[superpowers] failed to clear worktree state JSON:', err) } + // 连同 feature 分支一起删(本地 + 远程,best-effort)。受保护分支跳过、缺 token 只删本地, + // 逻辑与「拖到完成」共用同一个 cleanupFeatureBranch。 + const remote = await detectRepo(workspaceRoot) + const token = remote ? await getToken(panel.context, remote.host) : undefined + await cleanupFeatureBranch({ + workspaceRoot, + branch, + issueNumber, + devBranch: settingsForHook.devBranch, + autoBuildBranch: settingsForHook.autoBuildBranch, + remote: remote ? { host: remote.host, owner: remote.owner, repo: remote.repo } : undefined, + token, + }) + panel.postMessage({ type: 'issue/patch', issueNumber, @@ -119,7 +139,7 @@ export async function handleDeleteWorktree(panel: KanbanWebviewPanel, issueNumbe worktreeExists: false, }, }) - void window.showInformationMessage(`已删除 worktree #${issueNumber}`) + void window.showInformationMessage(`已删除 worktree #${issueNumber}${branch ? `(含分支 ${branch})` : ''}`) } /**