✨ 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 { getBrainstormPrompt } from '../../cc/prompts'
|
||||||
import { projectsDirFor, watchForNewSession } from '../../cc/sessionWatcher'
|
import { projectsDirFor, watchForNewSession } from '../../cc/sessionWatcher'
|
||||||
import { spawnClaude } from '../../cc/spawnClaude'
|
import { spawnClaude } from '../../cc/spawnClaude'
|
||||||
import { deleteLocalBranch, gitFetch } from '../../git/branchSync'
|
import { gitFetch } from '../../git/branchSync'
|
||||||
import { detectRepo } from '../../git/remote'
|
import { detectRepo } from '../../git/remote'
|
||||||
import {
|
import {
|
||||||
addDependency,
|
addDependency,
|
||||||
@@ -33,6 +33,7 @@ import { getSettings } from '../../settings/store'
|
|||||||
import { webhookCoordinator } from '../../webhook/coordinator'
|
import { webhookCoordinator } from '../../webhook/coordinator'
|
||||||
import { pickRandomIssueColor, themeColorIdToIconUri } from '../issueColor'
|
import { pickRandomIssueColor, themeColorIdToIconUri } from '../issueColor'
|
||||||
import { DEFAULT_PROFILE_PATH, makeNonce, PR_DIFF_SUMMARY_PROFILE_PATH } from '../KanbanPanel'
|
import { DEFAULT_PROFILE_PATH, makeNonce, PR_DIFF_SUMMARY_PROFILE_PATH } from '../KanbanPanel'
|
||||||
|
import { cleanupFeatureBranch } from './branchCleanup'
|
||||||
import * as sessions from './sessions'
|
import * as sessions from './sessions'
|
||||||
|
|
||||||
export async function handleColumnChange(panel: KanbanWebviewPanel, issueNumber: number, toColumn: IssueColumn): Promise<void> {
|
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) {
|
if (featureBranch) {
|
||||||
const settingsForBranchCleanup = getSettings(panel.context)
|
const settingsForBranchCleanup = getSettings(panel.context)
|
||||||
const protectedBranches = new Set(
|
await cleanupFeatureBranch({
|
||||||
['main', 'master', settingsForBranchCleanup.devBranch, settingsForBranchCleanup.autoBuildBranch]
|
workspaceRoot,
|
||||||
.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,
|
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。
|
// 6. 全部成功 → 增量推 done + 清掉 worktreePath + 标记 prMerged。
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
import { logger } from '../../logging/logger'
|
import { logger } from '../../logging/logger'
|
||||||
import { getSettings } from '../../settings/store'
|
import { getSettings } from '../../settings/store'
|
||||||
import { makeNonce } from '../KanbanPanel'
|
import { makeNonce } from '../KanbanPanel'
|
||||||
|
import { cleanupFeatureBranch } from './branchCleanup'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Open the worktree directory in a **new** VS Code window. The Boolean
|
* 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 里的
|
* Confirm + 递归强删 worktree 目录(removeWorktreeDir)→ 清掉 state JSON 里的
|
||||||
* `worktreePath`/`branch` 并刷新看板。删除失败(如权限、护栏拦截)把错误弹给用户。
|
* `worktreePath`/`branch` → 连同该工单的 feature 分支一起删(本地 + 远程,best-effort)
|
||||||
|
* → 刷新看板。目录删除失败(权限 / 护栏拦截)把错误弹给用户;分支清理失败只记日志、
|
||||||
|
* 不阻断——与「拖到完成」的清理行为一致。
|
||||||
*/
|
*/
|
||||||
export async function handleDeleteWorktree(panel: KanbanWebviewPanel, issueNumber: number, relPath: string): Promise<void> {
|
export async function handleDeleteWorktree(panel: KanbanWebviewPanel, issueNumber: number, relPath: string): Promise<void> {
|
||||||
const workspaceRoot = workspace.workspaceFolders?.[0]?.uri.fsPath
|
const workspaceRoot = workspace.workspaceFolders?.[0]?.uri.fsPath
|
||||||
@@ -68,8 +71,11 @@ export async function handleDeleteWorktree(panel: KanbanWebviewPanel, issueNumbe
|
|||||||
void window.showErrorMessage('请先打开一个工作区文件夹')
|
void window.showErrorMessage('请先打开一个工作区文件夹')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// 先读分支名:确认框要提示「连分支一起删」,删完目录后也据此清分支。
|
||||||
|
const state = await panel.readIssueState(issueNumber)
|
||||||
|
const branch = typeof state.branch === 'string' ? state.branch : ''
|
||||||
const choice = await window.showWarningMessage(
|
const choice = await window.showWarningMessage(
|
||||||
`确认删除 worktree ${relPath}?`,
|
branch ? `确认删除 worktree ${relPath} 及其分支 ${branch}?` : `确认删除 worktree ${relPath}?`,
|
||||||
{ modal: true },
|
{ modal: true },
|
||||||
'删除',
|
'删除',
|
||||||
)
|
)
|
||||||
@@ -85,7 +91,7 @@ export async function handleDeleteWorktree(panel: KanbanWebviewPanel, issueNumbe
|
|||||||
await dispatchWorktreeHook(panel, 'pre-remove', {
|
await dispatchWorktreeHook(panel, 'pre-remove', {
|
||||||
workspaceRoot,
|
workspaceRoot,
|
||||||
worktreePath: abs,
|
worktreePath: abs,
|
||||||
branch: '',
|
branch,
|
||||||
issueNumber,
|
issueNumber,
|
||||||
mainBranch: settingsForHook.devBranch || 'main',
|
mainBranch: settingsForHook.devBranch || 'main',
|
||||||
customScriptPath: settingsForHook.worktreePreRemoveScript,
|
customScriptPath: settingsForHook.worktreePreRemoveScript,
|
||||||
@@ -110,6 +116,20 @@ export async function handleDeleteWorktree(panel: KanbanWebviewPanel, issueNumbe
|
|||||||
console.warn('[superpowers] failed to clear worktree state JSON:', err)
|
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({
|
panel.postMessage({
|
||||||
type: 'issue/patch',
|
type: 'issue/patch',
|
||||||
issueNumber,
|
issueNumber,
|
||||||
@@ -119,7 +139,7 @@ export async function handleDeleteWorktree(panel: KanbanWebviewPanel, issueNumbe
|
|||||||
worktreeExists: false,
|
worktreeExists: false,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
void window.showInformationMessage(`已删除 worktree #${issueNumber}`)
|
void window.showInformationMessage(`已删除 worktree #${issueNumber}${branch ? `(含分支 ${branch})` : ''}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user