✨ feat(vscode): 支持重置回待办并修好待办依赖拖拽

This commit is contained in:
cruldra committed 2026-08-25 18:53:55 +08:00
1 parent 75dec8fa90
commit 6c05b7f7e3
12 files changed
+717 -164

No files matched your search

+315 -2
View File
@@ -21,6 +21,7 @@ import {
closeIssue,
deleteBranch,
deleteIssue,
getDependencies,
getPullRequest,
GiteaApiError,
mergePullRequest,
@@ -35,6 +36,7 @@ import { pickRandomIssueColor, themeColorIdToIconUri } from '../issueColor'
import { makeNonce } from '../KanbanPanel'
import { cleanupFeatureBranch } from './branchCleanup'
import * as sessions from './sessions'
import { canResetToTodo, todoResetStateExtra, todoResetUiPatch } from './todoReset'
export async function handleColumnChange(panel: KanbanWebviewPanel, issueNumber: number, toColumn: IssueColumn): Promise<void> {
if (toColumn === 'in-progress') {
@@ -42,6 +44,11 @@ export async function handleColumnChange(panel: KanbanWebviewPanel, issueNumber:
return
}
if (toColumn === 'todo') {
await handleResetToTodo(panel, issueNumber)
return
}
if (toColumn !== 'done') {
logger.add({
level: 'info',
@@ -554,6 +561,290 @@ export async function handleColumnChange(panel: KanbanWebviewPanel, issueNumber:
})
}
export async function handleResetToTodo(panel: KanbanWebviewPanel, issueNumber: number): Promise<void> {
const rollback = (fromColumn: IssueColumn | undefined): void => {
panel.postMessage({
type: 'issue/patch',
issueNumber,
patch: { column: fromColumn ?? 'in-progress' },
})
}
const workspaceRoot = workspace.workspaceFolders?.[0]?.uri.fsPath
if (!workspaceRoot) {
panel.postMessage({
type: 'toast/show',
id: makeNonce(),
level: 'error',
message: '请先打开一个工作区文件夹',
dismissOnTimer: 5000,
})
rollback(undefined)
return
}
const remote = await detectRepo(workspaceRoot)
if (!remote) {
panel.postMessage({
type: 'toast/show',
id: makeNonce(),
level: 'error',
message: '当前工作区没有 Gitea 远程仓库',
dismissOnTimer: 5000,
})
rollback(undefined)
return
}
const token = await getToken(panel.context, remote.host)
if (!token) {
panel.postMessage({
type: 'toast/show',
id: makeNonce(),
level: 'error',
message: '请先完成 Gitea 配置',
dismissOnTimer: 5000,
})
rollback(undefined)
return
}
// ① 读 state → 守卫(todo no-op / done 拒绝)→ 确认;取消 rollback 必须先有 fromColumn
let prStr: string | undefined
let worktreePath: string | undefined
let fromColumn: IssueColumn | undefined
let featureBranchFromState: string | undefined
try {
const stateObj = await panel.readIssueState(issueNumber)
if (typeof stateObj.pr === 'string' && stateObj.pr.length > 0)
prStr = stateObj.pr
if (typeof stateObj.worktreePath === 'string' && stateObj.worktreePath.length > 0)
worktreePath = stateObj.worktreePath
if (
typeof stateObj.column === 'string'
&& ['todo', 'in-progress', 'review', 'done'].includes(stateObj.column)
) {
fromColumn = stateObj.column as IssueColumn
}
if (typeof stateObj.branch === 'string' && stateObj.branch.length > 0)
featureBranchFromState = stateObj.branch
}
catch (err) {
const message = err instanceof Error ? err.message : String(err)
logger.add({
level: 'error',
source: 'panel',
message: `读取工单 #${issueNumber} 状态失败`,
details: message,
})
panel.postMessage({
type: 'toast/show',
id: makeNonce(),
level: 'error',
message: `读取工单 #${issueNumber} 状态失败: ${message}`,
dismissOnTimer: 6000,
})
rollback(undefined)
return
}
if (fromColumn === 'todo')
return
if (!canResetToTodo(fromColumn ?? 'in-progress')) {
panel.postMessage({
type: 'toast/show',
id: makeNonce(),
level: 'error',
message: `已完成的工单 #${issueNumber} 不能重置为待办`,
dismissOnTimer: 5000,
})
rollback(fromColumn)
return
}
const choice = await window.showWarningMessage(
`确定将工单 #${issueNumber} 重置为待办?将停止会话、删除 worktree / 未合并 PR / feature 分支,实施进度不可恢复。`,
{ modal: true },
'重置为待办',
)
if (choice !== '重置为待办') {
rollback(fromColumn)
return
}
// ② 停会话 → 删 worktree(失败即停)→ 未合并 PR 关掉 → 清 feature 分支
for (const [terminal, origin] of panel.terminalOrigin) {
if (origin.issueNumber === issueNumber) {
try {
terminal.dispose()
}
catch {
// dispose 失败也无所谓,VS Code 自己会清掉关闭事件
}
}
}
if (worktreePath) {
const abs = resolveWorktreePath(worktreePath, workspaceRoot)
if (fs.existsSync(abs)) {
const settingsForHook = getSettings(panel.context)
await panel.dispatchWorktreeHook('pre-remove', {
workspaceRoot,
worktreePath: abs,
branch: resolveFeatureBranch({ stateBranch: featureBranchFromState }),
issueNumber,
mainBranch: settingsForHook.devBranch || 'main',
customScriptPath: settingsForHook.worktreePreRemoveScript,
})
const killed = killProcessesUsingWorktree(abs)
if (killed.length > 0) {
const list = killed
.map(h => ` pid ${h.pid}: ${h.cmd.length > 80 ? `${h.cmd.slice(0, 80)}…` : h.cmd}`)
.join('\n')
logger.add({
level: 'warn',
source: 'panel',
message: `重置待办前已杀掉占用进程 (issue #${issueNumber})`,
details: list,
})
}
try {
await removeWorktreeDir(workspaceRoot, abs)
logger.add({
level: 'info',
source: 'panel',
message: `已清理 worktree ${worktreePath} (issue #${issueNumber})`,
})
}
catch (err) {
const message = err instanceof Error ? err.message : String(err)
logger.add({
level: 'error',
source: 'panel',
message: `清理 worktree 失败 (issue #${issueNumber})`,
details: message,
})
panel.postMessage({
type: 'toast/show',
id: makeNonce(),
level: 'error',
message: `删除 worktree 失败 #${issueNumber}: ${message}`,
dismissOnTimer: 6000,
})
rollback(fromColumn)
return
}
}
}
let prHeadRef: string | undefined
if (prStr) {
const prIndex = Number.parseInt(prStr, 10)
if (!Number.isFinite(prIndex)) {
panel.postMessage({
type: 'toast/show',
id: makeNonce(),
level: 'error',
message: `工单 #${issueNumber} 的 PR 字段无效: ${prStr}`,
dismissOnTimer: 6000,
})
rollback(fromColumn)
return
}
try {
const pullRequest = await getPullRequest({
host: remote.host,
token,
owner: remote.owner,
repo: remote.repo,
index: prIndex,
})
prHeadRef = pullRequest.headRef
if (!pullRequest.merged) {
await closeIssue({
host: remote.host,
token,
owner: remote.owner,
repo: remote.repo,
issueNumber: prIndex,
})
}
}
catch (err) {
const message = err instanceof Error ? err.message : String(err)
logger.add({
level: 'error',
source: 'panel',
message: `处理 PR #${prIndex} 失败 (issue #${issueNumber})`,
details: message,
})
panel.postMessage({
type: 'toast/show',
id: makeNonce(),
level: 'error',
message: `处理 PR #${prIndex} 失败 (issue #${issueNumber}): ${message}`,
dismissOnTimer: 6000,
})
rollback(fromColumn)
return
}
}
const featureBranch = resolveFeatureBranch({
stateBranch: featureBranchFromState,
prHeadRef,
})
if (featureBranch) {
const settingsForBranchCleanup = getSettings(panel.context)
await cleanupFeatureBranch({
workspaceRoot,
branch: featureBranch,
issueNumber,
devBranch: settingsForBranchCleanup.devBranch,
autoBuildBranch: settingsForBranchCleanup.autoBuildBranch,
remote,
token,
})
}
// ③ 落盘清空实施痕迹 → UI patch 用 null(postMessage 会丢 undefined)
try {
await panel.mergeIssueState(issueNumber, todoResetStateExtra())
}
catch (err) {
const message = err instanceof Error ? err.message : String(err)
logger.add({
level: 'error',
source: 'panel',
message: `持久化 column=todo 失败 (issue #${issueNumber})`,
details: message,
})
panel.postMessage({
type: 'toast/show',
id: makeNonce(),
level: 'error',
message: `保存工单 #${issueNumber} 状态失败: ${message}`,
dismissOnTimer: 6000,
})
rollback(fromColumn)
return
}
panel.postMessage({
type: 'issue/patch',
issueNumber,
patch: todoResetUiPatch(),
})
panel.postMessage({
type: 'toast/show',
id: makeNonce(),
level: 'success',
message: `工单 #${issueNumber} 已重置为待办`,
dismissOnTimer: 5000,
})
}
/**
* Handle a "drag to in-progress" kanban move.
*
@@ -1401,14 +1692,36 @@ export async function handleSetDependency(panel: KanbanWebviewPanel, issueNumber
}
try {
await addDependency({
// Gitea 允许多依赖,loader 只取第一个;拖到另一张卡片换父时必须先清掉旧前置。
const deps = await getDependencies({
host: remote.host,
token,
owner: remote.owner,
repo: remote.repo,
index: issueNumber,
dependencyIndex: prerequisiteNumber,
})
for (const dep of deps) {
if (dep.number !== prerequisiteNumber) {
await removeDependency({
host: remote.host,
token,
owner: remote.owner,
repo: remote.repo,
index: issueNumber,
dependencyIndex: dep.number,
})
}
}
if (!deps.some(d => d.number === prerequisiteNumber)) {
await addDependency({
host: remote.host,
token,
owner: remote.owner,
repo: remote.repo,
index: issueNumber,
dependencyIndex: prerequisiteNumber,
})
}
}
catch (err) {
const message = err instanceof Error ? err.message : String(err)