✨ feat(vscode): 手动删除 worktree 时连同 feature 分支一起删(本地+远程)
抽出共享 cleanupFeatureBranch(handlers/branchCleanup.ts):受保护分支跳过、 远程走 gitea API、本地强删(git branch -D),全程 best-effort 只记日志。 「拖到完成」的分支清理改为复用它(71 行 → 12 行);handleDeleteWorktree 在 删完 worktree 目录后同样调用它,确认框与完成提示带上分支名。 放在 handlers 层而非 git/branchSync.ts:后者刻意不依赖 vscode(有单测覆盖 deleteLocalBranch),本函数需 logger(→vscode)+ gitea API。
This commit is contained in:
@@ -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<void> {
|
||||
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),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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<void> {
|
||||
@@ -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,
|
||||
await cleanupFeatureBranch({
|
||||
workspaceRoot,
|
||||
branch: featureBranch,
|
||||
issueNumber,
|
||||
devBranch: settingsForBranchCleanup.devBranch,
|
||||
autoBuildBranch: settingsForBranchCleanup.autoBuildBranch,
|
||||
remote,
|
||||
token,
|
||||
})
|
||||
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,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 6. 全部成功 → 增量推 done + 清掉 worktreePath + 标记 prMerged。
|
||||
|
||||
@@ -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<void> {
|
||||
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})` : ''}`)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user