✨ feat(vscode): 发布按钮扩展侧 handler,删除分支同步
Claude-Session: https://claude.ai/code/session_01HTMgwaE3woqM5yvsHiuSAa
This commit is contained in:
1 parent
6f59b4f3fd
commit
b363e08ef9
12 files changed
+292
-345
No files matched your search
@@ -1,4 +1,4 @@
|
||||
import { deleteLocalBranch } from '../../git/branchSync'
|
||||
import { deleteLocalBranch } from '../../git/branches'
|
||||
import { deleteBranch } from '../../gitea/api'
|
||||
import { logger } from '../../logging/logger'
|
||||
|
||||
@@ -12,7 +12,7 @@ import { logger } from '../../logging/logger'
|
||||
* - 全程 best-effort:远程 / 本地各自 try/catch,失败只记日志——分支清理从不回滚已完成的主流程
|
||||
* (worktree 已删、PR 已合并)。branch 为空直接返回。
|
||||
*
|
||||
* 放在 handlers 层而非 `git/branchSync.ts`:后者刻意不依赖 `vscode`(有单测覆盖
|
||||
* 放在 handlers 层而非 `git/branches.ts`:后者刻意不依赖 `vscode`(有单测覆盖
|
||||
* deleteLocalBranch),而本函数要用 logger(→ vscode)和 gitea API。
|
||||
*/
|
||||
export async function cleanupFeatureBranch(opts: {
|
||||
|
||||
@@ -15,7 +15,7 @@ import { getBrainstormPrompt } from '../../cc/prompts'
|
||||
import { claudeProjectsRoot, installClaudeSession } from '../../cc/sessionBundle'
|
||||
import { projectsDirFor, watchForNewSession } from '../../cc/sessionWatcher'
|
||||
import { spawnClaude } from '../../cc/spawnClaude'
|
||||
import { gitFetch, resolveFeatureBranch } from '../../git/branchSync'
|
||||
import { gitFetch, resolveFeatureBranch } from '../../git/branches'
|
||||
import { detectRepo } from '../../git/remote'
|
||||
import { killProcessesUsingWorktree, removeWorktreeDir, resolveWorktreePath } from '../../git/worktree'
|
||||
import {
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
/**
|
||||
* 工具栏「发布」按钮的扩展侧 handler:切新发布分支、摘修复到当期发布分支、触发正式服部署。
|
||||
*
|
||||
* git 逻辑在 `src/git/release.ts`,Jenkins 触发在 `src/jenkins/trigger.ts`;这里只负责
|
||||
* 读设置、弹 QuickPick、发 toast、维护 `panel.releaseRunning` 互斥。
|
||||
*/
|
||||
|
||||
import type { KanbanWebviewPanel } from '../KanbanPanel'
|
||||
import { window, workspace } from 'vscode'
|
||||
import { gitFetch } from '../../git/branches'
|
||||
import {
|
||||
cherryPickToRelease,
|
||||
cutRelease,
|
||||
getReleaseStatus,
|
||||
listReleaseBranches,
|
||||
listUnreleasedCommits,
|
||||
todayReleaseBranch,
|
||||
} from '../../git/release'
|
||||
import { triggerJenkinsBuild } from '../../jenkins/trigger'
|
||||
import { getSettings } from '../../settings/store'
|
||||
import { dismiss, toast } from './handoffFlow'
|
||||
|
||||
export async function handleReleaseStatus(panel: KanbanWebviewPanel): Promise<void> {
|
||||
const root = workspace.workspaceFolders?.[0]?.uri.fsPath
|
||||
const s = getSettings(panel.context)
|
||||
const base = {
|
||||
type: 'release/status' as const,
|
||||
devBranch: s.devBranch,
|
||||
nextBranch: todayReleaseBranch(s.releaseBranchPrefix),
|
||||
running: panel.releaseRunning,
|
||||
}
|
||||
if (!root) {
|
||||
panel.postMessage({ ...base, unreleased: 0, unavailable: '请先打开一个工作区文件夹' })
|
||||
return
|
||||
}
|
||||
const st = await getReleaseStatus({ cwd: root, devBranch: s.devBranch, prefix: s.releaseBranchPrefix })
|
||||
panel.postMessage({ ...base, current: st.current, unreleased: st.unreleased, unavailable: st.unavailable })
|
||||
}
|
||||
|
||||
function requireRoot(): string {
|
||||
const root = workspace.workspaceFolders?.[0]?.uri.fsPath
|
||||
if (!root)
|
||||
throw new Error('请先打开一个工作区文件夹')
|
||||
return root
|
||||
}
|
||||
|
||||
/** 三个写动作共用:互斥 + 前后刷新状态 + 统一错误 toast */
|
||||
async function runReleaseAction(panel: KanbanWebviewPanel, fn: () => Promise<void>): Promise<void> {
|
||||
if (panel.releaseRunning)
|
||||
return
|
||||
panel.releaseRunning = true
|
||||
void handleReleaseStatus(panel)
|
||||
try {
|
||||
await fn()
|
||||
}
|
||||
catch (err) {
|
||||
toast(panel, 'error', err instanceof Error ? err.message : String(err), { dismissOnTimer: 10_000 })
|
||||
}
|
||||
finally {
|
||||
panel.releaseRunning = false
|
||||
void handleReleaseStatus(panel)
|
||||
}
|
||||
}
|
||||
|
||||
async function deploy(panel: KanbanWebviewPanel, branch: string): Promise<void> {
|
||||
const s = getSettings(panel.context)
|
||||
await triggerJenkinsBuild({ job: s.jenkinsJob, params: { REF: branch, REMOTE_HOST: s.jenkinsProdHost } })
|
||||
toast(panel, 'success', `已触发 Jenkins ${s.jenkinsJob}:REF=${branch} → ${s.jenkinsProdHost}`)
|
||||
}
|
||||
|
||||
export function handleReleaseCut(panel: KanbanWebviewPanel): Promise<void> {
|
||||
return runReleaseAction(panel, async () => {
|
||||
const root = requireRoot()
|
||||
const s = getSettings(panel.context)
|
||||
const branch = todayReleaseBranch(s.releaseBranchPrefix)
|
||||
const count = await cutRelease({ cwd: root, devBranch: s.devBranch, branch })
|
||||
toast(panel, 'success', `已切发布分支 ${branch}(${count} 个提交)`)
|
||||
if (!s.jenkinsJob)
|
||||
return
|
||||
const pick = await window.showInformationMessage(`现在部署 ${branch} 到${s.jenkinsProdHost}?`, '部署')
|
||||
if (pick === '部署')
|
||||
await deploy(panel, branch)
|
||||
})
|
||||
}
|
||||
|
||||
interface CommitPickItem { label: string, description: string, sha: string }
|
||||
|
||||
export function handleReleaseCherryPick(panel: KanbanWebviewPanel): Promise<void> {
|
||||
return runReleaseAction(panel, async () => {
|
||||
const root = requireRoot()
|
||||
const s = getSettings(panel.context)
|
||||
const status = await getReleaseStatus({ cwd: root, devBranch: s.devBranch, prefix: s.releaseBranchPrefix })
|
||||
if (status.unavailable)
|
||||
throw new Error(status.unavailable)
|
||||
const current = status.current
|
||||
if (!current)
|
||||
throw new Error('尚无发布分支,先切新发布')
|
||||
const commits = await listUnreleasedCommits({ cwd: root, devBranch: s.devBranch, current })
|
||||
if (commits.length === 0) {
|
||||
toast(panel, 'info', '没有待摘的提交')
|
||||
return
|
||||
}
|
||||
const picked = await pickCommits(current, commits.map(c => ({
|
||||
label: c.subject,
|
||||
description: `${c.sha.slice(0, 7)} · ${c.author} · ${c.date}`,
|
||||
sha: c.sha,
|
||||
})))
|
||||
if (!picked || picked.length === 0)
|
||||
return
|
||||
// 列表是 git log 顺序(新→旧),cherry-pick 要按时间顺序(旧→新)
|
||||
const order = new Map(commits.map((c, i) => [c.sha, i]))
|
||||
const shas = [...picked].sort((a, b) => (order.get(b.sha) ?? 0) - (order.get(a.sha) ?? 0)).map(p => p.sha)
|
||||
const spinner = toast(panel, 'info', `正在摘 ${shas.length} 个提交到 ${current}…`, { spinner: true, dismissOnTimer: 0 })
|
||||
try {
|
||||
await cherryPickToRelease({ cwd: root, branch: current, shas })
|
||||
}
|
||||
finally {
|
||||
dismiss(panel, spinner)
|
||||
}
|
||||
toast(panel, 'success', `已摘 ${shas.length} 个提交到 ${current}`)
|
||||
})
|
||||
}
|
||||
|
||||
/** 多选 QuickPick;用户按 Esc 返回 undefined */
|
||||
function pickCommits(current: string, items: CommitPickItem[]): Promise<CommitPickItem[] | undefined> {
|
||||
return new Promise((resolve) => {
|
||||
const qp = window.createQuickPick<CommitPickItem>()
|
||||
qp.canSelectMany = true
|
||||
qp.title = `摘修复到 ${current}`
|
||||
qp.placeholder = '勾选要摘到发布分支的提交'
|
||||
qp.items = items
|
||||
qp.onDidAccept(() => {
|
||||
resolve([...qp.selectedItems])
|
||||
qp.hide()
|
||||
})
|
||||
qp.onDidHide(() => {
|
||||
resolve(undefined)
|
||||
qp.dispose()
|
||||
})
|
||||
qp.show()
|
||||
})
|
||||
}
|
||||
|
||||
export function handleReleaseDeploy(panel: KanbanWebviewPanel): Promise<void> {
|
||||
return runReleaseAction(panel, async () => {
|
||||
const root = requireRoot()
|
||||
const s = getSettings(panel.context)
|
||||
if (!s.jenkinsJob)
|
||||
throw new Error('未配置 Jenkins 任务名,请先在设置里填写')
|
||||
await gitFetch(root)
|
||||
const branches = await listReleaseBranches({ cwd: root, prefix: s.releaseBranchPrefix })
|
||||
if (branches.length === 0)
|
||||
throw new Error('尚无发布分支')
|
||||
const [current, ...older] = branches
|
||||
const items = [
|
||||
{ label: current, description: '当期' },
|
||||
...older.map(label => ({ label, description: '回滚到此发布点' })),
|
||||
]
|
||||
const pick = await window.showQuickPick(items, {
|
||||
title: `部署到${s.jenkinsProdHost}`,
|
||||
placeHolder: '选择要部署的发布分支(默认当期,选旧的即回滚)',
|
||||
})
|
||||
if (!pick)
|
||||
return
|
||||
await deploy(panel, pick.label)
|
||||
})
|
||||
}
|
||||
@@ -202,8 +202,8 @@ export async function handleSettingsSave(panel: KanbanWebviewPanel, payload: {
|
||||
|| trimmedYtToken !== ''
|
||||
if ((!keepExisting && oldToken !== trimmedToken) || youtrackChanged)
|
||||
await panel.loadAndPush()
|
||||
// Branch-sync inputs may have changed — refresh the toolbar button.
|
||||
void panel.handleBranchSyncCheck()
|
||||
// 开发分支 / 发布前缀可能变了,刷新工具栏发布按钮。
|
||||
void panel.handleReleaseStatus()
|
||||
}
|
||||
|
||||
export async function handleEditSettingsRequest(panel: KanbanWebviewPanel): Promise<void> {
|
||||
|
||||
@@ -7,10 +7,9 @@ import { resolveCommitProfileForRun } from '../../cc/commitProfile'
|
||||
import { resolveProfilePath } from '../../cc/profiles'
|
||||
import { spawnClaude } from '../../cc/spawnClaude'
|
||||
import { findEnvFiles, lockEnvFiles, unlockEnvFiles } from '../../files/envLock'
|
||||
import { checkBranchSync, runBranchSync } from '../../git/branchSync'
|
||||
import { missingGitIdentityKeys, readGitIdentity } from '../../git/identity'
|
||||
import { logger } from '../../logging/logger'
|
||||
import { getEffectiveCommitProfilePath, getSettings } from '../../settings/store'
|
||||
import { getEffectiveCommitProfilePath } from '../../settings/store'
|
||||
import { makeNonce } from '../KanbanPanel'
|
||||
import { dismiss, toast } from './handoffFlow'
|
||||
|
||||
@@ -147,88 +146,6 @@ function formatCommitDone(resultText: string): string {
|
||||
return `提交完成:${summary}`
|
||||
}
|
||||
|
||||
export async function handleBranchSyncCheck(panel: KanbanWebviewPanel): Promise<void> {
|
||||
const workspaceRoot = workspace.workspaceFolders?.[0]?.uri.fsPath
|
||||
const s = getSettings(panel.context)
|
||||
const devBranch = s.devBranch
|
||||
// Empty string in storage means "follow devBranch" — resolve to
|
||||
// devBranch so the check naturally hits the "same branch" disabled
|
||||
// branch.
|
||||
const autoBuildBranch = s.autoBuildBranch.length > 0 ? s.autoBuildBranch : devBranch
|
||||
|
||||
if (!workspaceRoot) {
|
||||
panel.postMessage({
|
||||
type: 'branch-sync/status',
|
||||
behind: 0,
|
||||
devBranch,
|
||||
autoBuildBranch,
|
||||
unavailable: true,
|
||||
reason: '请先打开一个工作区文件夹',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const status = await checkBranchSync({ workspaceRoot, devBranch, autoBuildBranch })
|
||||
panel.postMessage({ type: 'branch-sync/status', ...status })
|
||||
}
|
||||
|
||||
export async function handleBranchSyncRun(panel: KanbanWebviewPanel): Promise<void> {
|
||||
const workspaceRoot = workspace.workspaceFolders?.[0]?.uri.fsPath
|
||||
const s = getSettings(panel.context)
|
||||
const devBranch = s.devBranch
|
||||
const autoBuildBranch = s.autoBuildBranch.length > 0 ? s.autoBuildBranch : devBranch
|
||||
|
||||
if (!workspaceRoot) {
|
||||
panel.postMessage({
|
||||
type: 'toast/show',
|
||||
id: makeNonce(),
|
||||
level: 'error',
|
||||
message: '请先打开一个工作区文件夹',
|
||||
dismissOnTimer: 5000,
|
||||
})
|
||||
void handleBranchSyncCheck(panel)
|
||||
return
|
||||
}
|
||||
|
||||
if (devBranch === autoBuildBranch) {
|
||||
panel.postMessage({
|
||||
type: 'toast/show',
|
||||
id: makeNonce(),
|
||||
level: 'info',
|
||||
message: '开发分支与自动化构建分支相同,无需同步',
|
||||
dismissOnTimer: 5000,
|
||||
})
|
||||
void handleBranchSyncCheck(panel)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await runBranchSync({ workspaceRoot, devBranch, autoBuildBranch })
|
||||
panel.postMessage({
|
||||
type: 'toast/show',
|
||||
id: makeNonce(),
|
||||
level: 'success',
|
||||
message: `已同步 ${devBranch} → ${autoBuildBranch}`,
|
||||
dismissOnTimer: 5000,
|
||||
})
|
||||
}
|
||||
catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err)
|
||||
panel.postMessage({
|
||||
type: 'toast/show',
|
||||
id: makeNonce(),
|
||||
level: 'error',
|
||||
message: `分支同步失败:${message}`,
|
||||
dismissOnTimer: 8000,
|
||||
})
|
||||
}
|
||||
finally {
|
||||
// Re-emit status regardless of success/failure so the button reflects
|
||||
// the new behind count (0 on success, unchanged on failure).
|
||||
void handleBranchSyncCheck(panel)
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleEnvLockCheck(panel: KanbanWebviewPanel): Promise<void> {
|
||||
const workspaceRoot = workspace.workspaceFolders?.[0]?.uri.fsPath
|
||||
// 用底层 get 不传默认值,能区分"从未设过"(undefined) 和"用户主动选 false"。
|
||||
|
||||
Reference in new issue
Block a user