♻️ refactor(vscode): 代码托管仓库抽成统一接口,Gitea 实现搬到 provider 后面,翻页改看 Link 头
This commit is contained in:
1 parent
c90d84d7f2
commit
14bee9f505
64 files changed
+1881
-2718
No files matched your search
@@ -1,5 +1,5 @@
|
||||
import type { ForgeContext } from '../../forge/context'
|
||||
import { deleteLocalBranch } from '../../git/branches'
|
||||
import { deleteBranch } from '../../gitea/api'
|
||||
import { logger } from '../../logging/logger'
|
||||
|
||||
/**
|
||||
@@ -20,11 +20,10 @@ export async function cleanupFeatureBranch(opts: {
|
||||
branch: string
|
||||
issueNumber: number
|
||||
devBranch?: string
|
||||
/** 缺 remote 或 token 时跳过远程删除,只删本地。 */
|
||||
remote?: { host: string, owner: string, repo: string }
|
||||
token?: string
|
||||
/** 没有代码托管仓库时跳过远程删除,只删本地。 */
|
||||
forge?: ForgeContext
|
||||
}): Promise<void> {
|
||||
const { workspaceRoot, branch, issueNumber, devBranch, remote, token } = opts
|
||||
const { workspaceRoot, branch, issueNumber, devBranch, forge } = opts
|
||||
if (!branch)
|
||||
return
|
||||
|
||||
@@ -36,9 +35,9 @@ export async function cleanupFeatureBranch(opts: {
|
||||
return
|
||||
}
|
||||
|
||||
if (remote && token) {
|
||||
if (forge) {
|
||||
try {
|
||||
await deleteBranch({ host: remote.host, token, owner: remote.owner, repo: remote.repo, branch })
|
||||
await forge.provider.deleteBranch(branch)
|
||||
logger.add({ level: 'info', source: 'panel', message: `已删除远程 feature 分支 ${branch} (issue #${issueNumber})` })
|
||||
}
|
||||
catch (err) {
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { ForgeContext } from '../../forge/context'
|
||||
import type { KanbanWebviewPanel } from '../KanbanPanel'
|
||||
import { window } from 'vscode'
|
||||
import { resolveForge } from '../../forge/context'
|
||||
import { makeNonce } from '../../webview/html'
|
||||
|
||||
/** 解析当前工作区的代码托管仓库;解析不出来就弹 toast 说明原因,返回 `undefined`。 */
|
||||
export async function forgeOrToast(panel: KanbanWebviewPanel): Promise<ForgeContext | undefined> {
|
||||
const resolved = await resolveForge(panel.context)
|
||||
if (resolved.ok)
|
||||
return resolved.forge
|
||||
panel.postMessage({ type: 'toast/show', id: makeNonce(), level: 'error', message: resolved.message, dismissOnTimer: 5000 })
|
||||
return undefined
|
||||
}
|
||||
|
||||
/** 同 [forgeOrToast],但用 VS Code 的错误弹窗说明原因,给从命令或会话入口进来、不一定看得到看板的操作用。 */
|
||||
export async function forgeOrErrorMessage(panel: KanbanWebviewPanel): Promise<ForgeContext | undefined> {
|
||||
const resolved = await resolveForge(panel.context)
|
||||
if (resolved.ok)
|
||||
return resolved.forge
|
||||
void window.showErrorMessage(resolved.message)
|
||||
return undefined
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { HandoffSessions } from '../../cc/handoffManifest'
|
||||
import type { IssueColumn } from '../../gitea/types'
|
||||
import type { IssueColumn } from '../../issues/types'
|
||||
import { formatByteSize } from '../../cc/handoffArchive'
|
||||
import { LOCAL_PROFILE_FIELDS, LOCAL_STATE_FIELDS } from '../../issues/localState'
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { HandoffManifest, HandoffSessions } from '../../cc/handoffManifest'
|
||||
import type { IssueColumn } from '../../gitea/types'
|
||||
import type { ForgeContext } from '../../forge/context'
|
||||
import type { IssueColumn } from '../../issues/types'
|
||||
import type { HandoffStore } from '../../oss/handoffStore'
|
||||
import type { KanbanWebviewPanel } from '../KanbanPanel'
|
||||
import { execFile } from 'node:child_process'
|
||||
@@ -7,18 +8,15 @@ import * as fs from 'node:fs'
|
||||
import { promises as fsp } from 'node:fs'
|
||||
import * as os from 'node:os'
|
||||
import * as path from 'node:path'
|
||||
import { window, workspace } from 'vscode'
|
||||
import { loginForToken } from '../../auth/identity'
|
||||
import { getToken } from '../../auth/secrets'
|
||||
import { window } from 'vscode'
|
||||
import { loginFor } from '../../auth/identity'
|
||||
import { formatByteSize, packHandoff, unpackHandoff } from '../../cc/handoffArchive'
|
||||
import { installHandoffBundle, stageHandoffBundle } from '../../cc/handoffBundle'
|
||||
import { HANDOFF_MANIFEST_FILE, handoffBundleKey, parseHandoffManifest } from '../../cc/handoffManifest'
|
||||
import { claudeProjectsRoot } from '../../cc/sessionBundle'
|
||||
import { projectsDirFor } from '../../cc/sessionWatcher'
|
||||
import { detectRepo } from '../../git/remote'
|
||||
import { ensureWorktreeFromRemote, killProcessesUsingWorktree, removeWorktreeDir, resolveWorktreePath } from '../../git/worktree'
|
||||
import { DEFAULT_WORKTREE_TEMPLATE, resolveWorktreeDir, slugFromBranch } from '../../git/worktreePath'
|
||||
import { getIssue, listRepoAssignees, updateIssueAssignees } from '../../gitea/api'
|
||||
import { logger } from '../../logging/logger'
|
||||
import { deleteHandoffBundle, downloadHandoffBundle, uploadHandoffBundle } from '../../oss/handoffStore'
|
||||
import { HANDOFF_STORE_MISSING_MESSAGE, readHandoffStore } from '../../oss/handoffStoreSetup'
|
||||
@@ -26,6 +24,7 @@ import { annotateMissingSessionIds, defaultCodexSessionsDir } from '../../sessio
|
||||
import { getSettings } from '../../settings/store'
|
||||
import { makeNonce } from '../../webview/html'
|
||||
import { cleanupFeatureBranch } from './branchCleanup'
|
||||
import { forgeOrToast } from './forgeGate'
|
||||
import {
|
||||
canAcceptHandoff,
|
||||
canStartHandoff,
|
||||
@@ -39,12 +38,8 @@ import {
|
||||
} from './handoff'
|
||||
import { canAnswerHandoffRequest, canAskHandoff, canCancelHandoffRequest } from './handoffRequest'
|
||||
|
||||
export interface RepoCtx {
|
||||
workspaceRoot: string
|
||||
host: string
|
||||
owner: string
|
||||
repo: string
|
||||
token: string
|
||||
/** 当前工作区的代码托管仓库,加上本机凭据对应的 login。 */
|
||||
export interface RepoCtx extends ForgeContext {
|
||||
me: string
|
||||
}
|
||||
|
||||
@@ -59,27 +54,15 @@ export function dismiss(panel: KanbanWebviewPanel, id: string): void {
|
||||
}
|
||||
|
||||
export async function resolveRepoCtx(panel: KanbanWebviewPanel): Promise<RepoCtx | undefined> {
|
||||
const workspaceRoot = workspace.workspaceFolders?.[0]?.uri.fsPath
|
||||
if (!workspaceRoot) {
|
||||
toast(panel, 'error', '请先打开一个工作区文件夹')
|
||||
const forge = await forgeOrToast(panel)
|
||||
if (!forge)
|
||||
return undefined
|
||||
}
|
||||
const remote = await detectRepo(workspaceRoot)
|
||||
if (!remote) {
|
||||
toast(panel, 'error', '当前工作区没有 Gitea 远程仓库')
|
||||
return undefined
|
||||
}
|
||||
const token = await getToken(panel.context, remote.host)
|
||||
if (!token) {
|
||||
toast(panel, 'error', '请先完成 Gitea 配置')
|
||||
return undefined
|
||||
}
|
||||
const me = await loginForToken(remote.host, token)
|
||||
const me = await loginFor(forge)
|
||||
if (!me) {
|
||||
toast(panel, 'error', '无法解析当前 Gitea 身份,token 是否失效?')
|
||||
toast(panel, 'error', '无法解析当前身份,token 是否失效?')
|
||||
return undefined
|
||||
}
|
||||
return { workspaceRoot, host: remote.host, owner: remote.owner, repo: remote.repo, token, me }
|
||||
return { ...forge, me }
|
||||
}
|
||||
|
||||
function git(cwd: string, args: string[]): Promise<{ ok: boolean, stdout: string, stderr: string }> {
|
||||
@@ -135,7 +118,7 @@ export async function handleHandoffUsers(panel: KanbanWebviewPanel, issueNumber:
|
||||
return
|
||||
}
|
||||
try {
|
||||
const users = await listRepoAssignees(ctx)
|
||||
const users = await ctx.provider.listAssignees()
|
||||
panel.postMessage({
|
||||
type: 'handoff/users-result',
|
||||
issueNumber,
|
||||
@@ -172,7 +155,7 @@ export async function handleHandoffStart(panel: KanbanWebviewPanel, issueNumber:
|
||||
const ctx = await resolveRepoCtx(panel)
|
||||
if (!ctx)
|
||||
return { ok: false, message: '无法开始移交' }
|
||||
const { workspaceRoot, host, owner, repo, token, me } = ctx
|
||||
const { workspaceRoot, owner, repo, provider, me } = ctx
|
||||
|
||||
let state: Record<string, unknown>
|
||||
try {
|
||||
@@ -186,7 +169,7 @@ export async function handleHandoffStart(panel: KanbanWebviewPanel, issueNumber:
|
||||
|
||||
// 先拉远端 issue:既要兜底「Gitea 已关闭但从没拖进看板」的工单,也要拿到
|
||||
// assignees 判断「是否我负责」——两者都得在 canStartHandoff 判定前就绪。
|
||||
const issue = await getIssue({ host, token, owner, repo, index: issueNumber })
|
||||
const issue = await provider.getIssue(issueNumber)
|
||||
if (!issue) {
|
||||
toast(panel, 'error', `工单 #${issueNumber} 不存在或已删除`)
|
||||
return { ok: false, message: `工单 #${issueNumber} 不存在或已删除` }
|
||||
@@ -201,7 +184,7 @@ export async function handleHandoffStart(panel: KanbanWebviewPanel, issueNumber:
|
||||
// 避免重复移交或抢别人的工单。
|
||||
const knownColumns: IssueColumn[] = ['todo', 'in-progress', 'review', 'done']
|
||||
const column = knownColumns.includes(state.column as IssueColumn) ? (state.column as IssueColumn) : 'todo'
|
||||
const assignees = (issue.assignees ?? []).map(a => a.login)
|
||||
const assignees = issue.assignees.map(a => a.login)
|
||||
if (!canStartHandoff({ source: 'gitea', column, handoffBundleKey: str(state.handoffBundleKey), assignees }, me)) {
|
||||
const message = `#${issueNumber} 不是你负责的工单,或已在移交中 / 已完成,不能移交`
|
||||
toast(panel, 'error', message)
|
||||
@@ -325,7 +308,7 @@ export async function handleHandoffStart(panel: KanbanWebviewPanel, issueNumber:
|
||||
}
|
||||
say('指派')
|
||||
try {
|
||||
await updateIssueAssignees({ host, token, owner, repo, index: issueNumber, assignees: [to] })
|
||||
await provider.setIssueAssignees(issueNumber, [to])
|
||||
}
|
||||
catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err)
|
||||
@@ -406,7 +389,7 @@ async function requestFacts(panel: KanbanWebviewPanel, issueNumber: number): Pro
|
||||
toast(panel, 'error', `读取工单 #${issueNumber} 状态失败:${err instanceof Error ? err.message : String(err)}`)
|
||||
return
|
||||
}
|
||||
const issue = await getIssue({ host: ctx.host, token: ctx.token, owner: ctx.owner, repo: ctx.repo, index: issueNumber })
|
||||
const issue = await ctx.provider.getIssue(issueNumber)
|
||||
if (!issue) {
|
||||
toast(panel, 'error', `工单 #${issueNumber} 不存在或已删除`)
|
||||
return
|
||||
@@ -420,7 +403,7 @@ async function requestFacts(panel: KanbanWebviewPanel, issueNumber: number): Pro
|
||||
column,
|
||||
...(str(state.handoffBundleKey) ? { handoffBundleKey: str(state.handoffBundleKey) } : {}),
|
||||
...(str(state.handoffRequestFrom) ? { handoffRequestFrom: str(state.handoffRequestFrom) } : {}),
|
||||
assignees: (issue.assignees ?? []).map(a => a.login),
|
||||
assignees: issue.assignees.map(a => a.login),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -497,7 +480,7 @@ export async function handleHandoffAccept(panel: KanbanWebviewPanel, issueNumber
|
||||
finish()
|
||||
return
|
||||
}
|
||||
const { workspaceRoot, host, owner, repo, token, me } = ctx
|
||||
const { workspaceRoot, provider, me } = ctx
|
||||
|
||||
let state: Record<string, unknown>
|
||||
try {
|
||||
@@ -525,13 +508,13 @@ export async function handleHandoffAccept(panel: KanbanWebviewPanel, issueNumber
|
||||
|
||||
// 前置复查:webview 按钮的显示条件(assignees 含 me)可能已经过期(工单被
|
||||
// 重新指派、被删),用刚从 Gitea 拉到的最新 issue 再判一次 canAcceptHandoff。
|
||||
const issue = await getIssue({ host, token, owner, repo, index: issueNumber })
|
||||
const issue = await provider.getIssue(issueNumber)
|
||||
if (!issue) {
|
||||
progress.dismiss()
|
||||
toast(panel, 'error', `工单 #${issueNumber} 不存在或已删除`)
|
||||
return
|
||||
}
|
||||
if (!canAcceptHandoff({ handoffBundleKey: bundleKey, assignees: (issue.assignees ?? []).map(a => a.login) }, me)) {
|
||||
if (!canAcceptHandoff({ handoffBundleKey: bundleKey, assignees: issue.assignees.map(a => a.login) }, me)) {
|
||||
progress.dismiss()
|
||||
toast(panel, 'error', `#${issueNumber} 未指派给你,不能接管`)
|
||||
return
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { GiteaComment } from '../../gitea/api'
|
||||
import type { ForgeComment } from '../../forge/types'
|
||||
import type { KanbanWebviewPanel } from '../KanbanPanel'
|
||||
import { listIssueComments, postIssueComment } from '../../gitea/api'
|
||||
import { systemCommentText } from '../../gitea/issueComment'
|
||||
import { systemCommentText } from '../../forge/issueComment'
|
||||
import { logger } from '../../logging/logger'
|
||||
import { resolveRepoCtx, toast } from './handoffFlow'
|
||||
|
||||
@@ -15,14 +14,14 @@ export interface IssueChatComment {
|
||||
systemText?: string
|
||||
}
|
||||
|
||||
function toChat(comment: GiteaComment): IssueChatComment {
|
||||
function toChat(comment: ForgeComment): IssueChatComment {
|
||||
const systemText = systemCommentText(comment.body)
|
||||
return {
|
||||
id: comment.id,
|
||||
author: comment.user?.login ?? '未知',
|
||||
authorName: comment.user?.full_name || undefined,
|
||||
avatarUrl: comment.user?.avatar_url || undefined,
|
||||
createdAt: comment.created_at,
|
||||
author: comment.author?.login || '未知',
|
||||
authorName: comment.author?.fullName,
|
||||
avatarUrl: comment.author?.avatarUrl,
|
||||
createdAt: comment.createdAt,
|
||||
body: comment.body,
|
||||
...(systemText ? { systemText } : {}),
|
||||
}
|
||||
@@ -32,13 +31,7 @@ async function pushComments(panel: KanbanWebviewPanel, issueNumber: number): Pro
|
||||
const ctx = await resolveRepoCtx(panel)
|
||||
if (!ctx)
|
||||
return
|
||||
const comments = await listIssueComments({
|
||||
host: ctx.host,
|
||||
token: ctx.token,
|
||||
owner: ctx.owner,
|
||||
repo: ctx.repo,
|
||||
index: issueNumber,
|
||||
})
|
||||
const comments = await ctx.provider.listIssueComments(issueNumber)
|
||||
panel.postMessage({
|
||||
type: 'issue/comments-result',
|
||||
issueNumber,
|
||||
@@ -65,14 +58,7 @@ export async function handleIssueCommentPost(panel: KanbanWebviewPanel, issueNum
|
||||
if (!ctx)
|
||||
return
|
||||
try {
|
||||
await postIssueComment({
|
||||
host: ctx.host,
|
||||
token: ctx.token,
|
||||
owner: ctx.owner,
|
||||
repo: ctx.repo,
|
||||
index: issueNumber,
|
||||
body: text,
|
||||
})
|
||||
await ctx.provider.postIssueComment(issueNumber, text)
|
||||
await pushComments(panel, issueNumber)
|
||||
}
|
||||
catch (err) {
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,14 +1,15 @@
|
||||
/**
|
||||
* 「改动」tab 的扩展侧 handler:实时从 Gitea 读整个 PR 的改动文件清单,并按需取
|
||||
* 「改动」tab 的扩展侧 handler:实时从代码托管仓库读整个 PR 的改动文件清单,并按需取
|
||||
* 单个文件改动前后的原文,交给 webview 内联渲染(主从式 diff)。
|
||||
*
|
||||
* 数据全程实时拉取,只把「已查看」确认态落到 `.spx/pr-review-confirmed.json`
|
||||
* (按文件变更指纹失效:作者推新提交后,仅真改动过的文件掉勾,没动的保留)。diff
|
||||
* 左右两侧用 merge_base → head 的原文,与 Gitea 网页 diff 口径一致;getRawFile 对
|
||||
* 左右两侧用 merge_base → head 的原文,与网页上的 PR diff 口径一致;getRawFile 对
|
||||
* 404 兜底空串,故新增/删除/改名天然成立。
|
||||
*/
|
||||
|
||||
import type { PrDiffMode } from '../../cc/prDiffMode'
|
||||
import type { ForgeProvider, ForgePullRequest } from '../../forge/types'
|
||||
import type { KanbanWebviewPanel } from '../KanbanPanel'
|
||||
import { accessSync, constants } from 'node:fs'
|
||||
import { createPatch } from 'diff'
|
||||
@@ -17,26 +18,31 @@ import { resolveCommitProfileForRun } from '../../cc/commitProfile'
|
||||
import { parsePrDiffMode } from '../../cc/prDiffMode'
|
||||
import { resolveProfilePath } from '../../cc/profiles'
|
||||
import { spawnClaude } from '../../cc/spawnClaude'
|
||||
import { getPullRequest, getRawFile, listPullRequestFiles } from '../../gitea/api'
|
||||
import { resolveRepoContext } from '../../gitea/repoContext'
|
||||
import { readStateJsonComment } from '../../gitea/stateJson'
|
||||
import { resolveForge } from '../../forge/context'
|
||||
import { readFileSummaries, readSummarySession, writeFileSummary, writeSummarySession } from '../../sessions/prFileSummaryStore'
|
||||
import { readConfirmedSigs, writeConfirmedSigs } from '../../sessions/prReviewStore'
|
||||
import { getEffectivePrDiffSummaryProfilePath } from '../../settings/store'
|
||||
import { makeNonce } from '../../webview/html'
|
||||
|
||||
/** 从工单的 state JSON 取 PR 号。无则返回 undefined(前端按空态处理,不算错误)。 */
|
||||
async function resolvePrNumber(
|
||||
ctx: { host: string, owner: string, repo: string, token: string },
|
||||
issueNumber: number,
|
||||
): Promise<number | undefined> {
|
||||
const stateObj = await readStateJsonComment({
|
||||
host: ctx.host,
|
||||
owner: ctx.owner,
|
||||
repo: ctx.repo,
|
||||
token: ctx.token,
|
||||
issueNumber,
|
||||
})
|
||||
/** PR 里一个文件改动前后的原文:左边是合并基上的 `oldPath`,右边是 head 上的 `path`。 */
|
||||
async function prFileSides(
|
||||
provider: ForgeProvider,
|
||||
prNumber: number,
|
||||
path: string,
|
||||
oldPath: string,
|
||||
): Promise<{ pr: ForgePullRequest, oldContent: string, newContent: string }> {
|
||||
const pr = await provider.getPullRequest(prNumber)
|
||||
const base = await provider.pullRequestDiffBase(pr)
|
||||
const [oldContent, newContent] = await Promise.all([
|
||||
provider.getRawFile(oldPath, base),
|
||||
provider.getRawFile(path, pr.headSha),
|
||||
])
|
||||
return { pr, oldContent, newContent }
|
||||
}
|
||||
|
||||
async function resolvePrNumber(panel: KanbanWebviewPanel, issueNumber: number): Promise<number | undefined> {
|
||||
const stateObj = await panel.readIssueState(issueNumber)
|
||||
const pr = typeof stateObj?.pr === 'string' && stateObj.pr.length > 0 ? stateObj.pr : undefined
|
||||
return pr ? Number(pr) : undefined
|
||||
}
|
||||
@@ -104,45 +110,34 @@ function fileSig(f: { status: string, additions: number, deletions: number }): s
|
||||
*/
|
||||
export async function handleGetPrFiles(panel: KanbanWebviewPanel, issueNumber: number): Promise<void> {
|
||||
try {
|
||||
const ctx = await resolveRepoContext(panel.context)
|
||||
if (!ctx) {
|
||||
const resolved = await resolveForge(panel.context)
|
||||
if (!resolved.ok) {
|
||||
panel.postMessage({ type: 'pr-files/show', issueNumber, files: [], confirmed: [], summaries: {} })
|
||||
return
|
||||
}
|
||||
|
||||
const index = await resolvePrNumber(ctx, issueNumber)
|
||||
const index = await resolvePrNumber(panel, issueNumber)
|
||||
if (index === undefined) {
|
||||
panel.postMessage({ type: 'pr-files/show', issueNumber, files: [], confirmed: [], summaries: {} })
|
||||
return
|
||||
}
|
||||
|
||||
const files = await listPullRequestFiles({
|
||||
host: ctx.host,
|
||||
owner: ctx.owner,
|
||||
repo: ctx.repo,
|
||||
token: ctx.token,
|
||||
index,
|
||||
})
|
||||
const files = await resolved.forge.provider.listPullRequestFiles(index)
|
||||
|
||||
const workspaceRoot = workspace.workspaceFolders?.[0]?.uri.fsPath
|
||||
let confirmed: string[] = []
|
||||
let summaries: Record<string, string> = {}
|
||||
if (workspaceRoot) {
|
||||
const [stored, sums] = await Promise.all([
|
||||
readConfirmedSigs(workspaceRoot, issueNumber),
|
||||
readFileSummaries(workspaceRoot, issueNumber),
|
||||
])
|
||||
summaries = sums
|
||||
// 当前每文件的变更指纹;仅指纹相符的已查看条目仍有效。
|
||||
const cur = new Map(files.map(f => [f.path, fileSig(f)]))
|
||||
confirmed = Object.keys(stored).filter(p => cur.get(p) === stored[p])
|
||||
// 有失配/消失才写盘,剪掉陈旧条目,避免无谓磁盘写入。
|
||||
if (confirmed.length !== Object.keys(stored).length) {
|
||||
const pruned: Record<string, string> = {}
|
||||
for (const p of confirmed)
|
||||
pruned[p] = stored[p]
|
||||
await writeConfirmedSigs(workspaceRoot, issueNumber, pruned)
|
||||
}
|
||||
const { workspaceRoot } = resolved.forge
|
||||
const [stored, summaries] = await Promise.all([
|
||||
readConfirmedSigs(workspaceRoot, issueNumber),
|
||||
readFileSummaries(workspaceRoot, issueNumber),
|
||||
])
|
||||
// 当前每文件的变更指纹;仅指纹相符的已查看条目仍有效。
|
||||
const cur = new Map(files.map(f => [f.path, fileSig(f)]))
|
||||
const confirmed = Object.keys(stored).filter(p => cur.get(p) === stored[p])
|
||||
// 有失配/消失才写盘,剪掉陈旧条目,避免无谓磁盘写入。
|
||||
if (confirmed.length !== Object.keys(stored).length) {
|
||||
const pruned: Record<string, string> = {}
|
||||
for (const p of confirmed)
|
||||
pruned[p] = stored[p]
|
||||
await writeConfirmedSigs(workspaceRoot, issueNumber, pruned)
|
||||
}
|
||||
|
||||
panel.postMessage({ type: 'pr-files/show', issueNumber, files, confirmed, summaries })
|
||||
@@ -168,24 +163,19 @@ export async function handleGetPrFileDiff(
|
||||
args: { issueNumber: number, path: string, previousPath?: string },
|
||||
): Promise<void> {
|
||||
try {
|
||||
const ctx = await resolveRepoContext(panel.context)
|
||||
if (!ctx) {
|
||||
const resolved = await resolveForge(panel.context)
|
||||
if (!resolved.ok) {
|
||||
panel.postMessage({ type: 'pr-file-diff/show', issueNumber: args.issueNumber, path: args.path, oldContent: '', newContent: '' })
|
||||
return
|
||||
}
|
||||
const index = await resolvePrNumber(ctx, args.issueNumber)
|
||||
const index = await resolvePrNumber(panel, args.issueNumber)
|
||||
if (index === undefined) {
|
||||
panel.postMessage({ type: 'pr-file-diff/show', issueNumber: args.issueNumber, path: args.path, oldContent: '', newContent: '' })
|
||||
return
|
||||
}
|
||||
|
||||
const pr = await getPullRequest({ host: ctx.host, owner: ctx.owner, repo: ctx.repo, token: ctx.token, index })
|
||||
const mergeBase = pr.mergeBase || pr.baseSha
|
||||
const oldPath = args.previousPath ?? args.path
|
||||
const [oldContent, newContent] = await Promise.all([
|
||||
getRawFile({ host: ctx.host, owner: ctx.owner, repo: ctx.repo, token: ctx.token, filepath: oldPath, ref: mergeBase }),
|
||||
getRawFile({ host: ctx.host, owner: ctx.owner, repo: ctx.repo, token: ctx.token, filepath: args.path, ref: pr.headSha }),
|
||||
])
|
||||
const { oldContent, newContent } = await prFileSides(resolved.forge.provider, index, args.path, oldPath)
|
||||
panel.postMessage({
|
||||
type: 'pr-file-diff/show',
|
||||
issueNumber: args.issueNumber,
|
||||
@@ -256,12 +246,12 @@ export async function handleGeneratePrFileSummary(
|
||||
): Promise<void> {
|
||||
const { issueNumber, path: filePath } = args
|
||||
try {
|
||||
const workspaceRoot = workspace.workspaceFolders?.[0]?.uri.fsPath
|
||||
const ctx = await resolveRepoContext(panel.context)
|
||||
if (!ctx || !workspaceRoot) {
|
||||
panel.postMessage({ type: 'pr-file-summary/show', issueNumber, path: filePath, error: '当前工作区没有 Gitea 仓库或未配置 token' })
|
||||
const resolved = await resolveForge(panel.context)
|
||||
if (!resolved.ok) {
|
||||
panel.postMessage({ type: 'pr-file-summary/show', issueNumber, path: filePath, error: resolved.message })
|
||||
return
|
||||
}
|
||||
const { workspaceRoot } = resolved.forge
|
||||
|
||||
const raw = await getEffectivePrDiffSummaryProfilePath(panel.context)
|
||||
const resolution = resolveCommitProfileForRun(
|
||||
@@ -292,19 +282,13 @@ export async function handleGeneratePrFileSummary(
|
||||
return
|
||||
}
|
||||
|
||||
const index = await resolvePrNumber(ctx, issueNumber)
|
||||
const index = await resolvePrNumber(panel, issueNumber)
|
||||
if (index === undefined) {
|
||||
panel.postMessage({ type: 'pr-file-summary/show', issueNumber, path: filePath, error: '该工单还没有 PR' })
|
||||
return
|
||||
}
|
||||
|
||||
const pr = await getPullRequest({ host: ctx.host, owner: ctx.owner, repo: ctx.repo, token: ctx.token, index })
|
||||
const mergeBase = pr.mergeBase || pr.baseSha
|
||||
const oldPath = args.previousPath ?? filePath
|
||||
const [oldContent, newContent] = await Promise.all([
|
||||
getRawFile({ host: ctx.host, owner: ctx.owner, repo: ctx.repo, token: ctx.token, filepath: oldPath, ref: mergeBase }),
|
||||
getRawFile({ host: ctx.host, owner: ctx.owner, repo: ctx.repo, token: ctx.token, filepath: filePath, ref: pr.headSha }),
|
||||
])
|
||||
const { pr, oldContent, newContent } = await prFileSides(resolved.forge.provider, index, filePath, args.previousPath ?? filePath)
|
||||
const patch = createPatch(filePath, oldContent, newContent, '', '')
|
||||
|
||||
const prevSession = await readSummarySession(workspaceRoot, issueNumber)
|
||||
|
||||
@@ -31,7 +31,6 @@ import {
|
||||
sessionInboxPrefix,
|
||||
} from '../../cc/sessionHandoff'
|
||||
import { projectsDirFor } from '../../cc/sessionWatcher'
|
||||
import { listRepoAssignees } from '../../gitea/api'
|
||||
import { logger } from '../../logging/logger'
|
||||
import {
|
||||
deleteHandoffBundle,
|
||||
@@ -52,7 +51,7 @@ export async function handleSessionHandoffUsers(panel: KanbanWebviewPanel): Prom
|
||||
return
|
||||
}
|
||||
try {
|
||||
const users = await listRepoAssignees(ctx)
|
||||
const users = await ctx.provider.listAssignees()
|
||||
panel.postMessage({
|
||||
type: 'session-handoff/users-result',
|
||||
users: users.map(u => u.login).filter(l => l !== ctx.me).sort(),
|
||||
|
||||
@@ -6,18 +6,16 @@ import * as fs from 'node:fs'
|
||||
import { promises as fsp } from 'node:fs'
|
||||
import * as path from 'node:path'
|
||||
import { window, workspace } from 'vscode'
|
||||
import { loginForToken } from '../../auth/identity'
|
||||
import { getToken } from '../../auth/secrets'
|
||||
import { loginFor } from '../../auth/identity'
|
||||
import { buildCcArgv } from '../../cc/ccCommand'
|
||||
import { buildCodexArgv } from '../../cc/codexCommand'
|
||||
import { resolveProfilePath } from '../../cc/profiles'
|
||||
import { renderBrainstormPrompt, renderImplementPlanPrompt, renderPrototypePrompt, renderReviewPrompt, renderTestPrompt } from '../../cc/prompts'
|
||||
import { sessionLaunch } from '../../cc/sessionTerminal'
|
||||
import { projectsDirFor, watchForNewSession } from '../../cc/sessionWatcher'
|
||||
import { detectRepo } from '../../git/remote'
|
||||
import { resolveForge } from '../../forge/context'
|
||||
import { ensureWorktree, resolveWorktreePath } from '../../git/worktree'
|
||||
import { DEFAULT_WORKTREE_TEMPLATE, deriveSlug, resolveWorktreeDir } from '../../git/worktreePath'
|
||||
import { updateIssueAssignees } from '../../gitea/api'
|
||||
import { getLocalIssueState } from '../../issues/localState'
|
||||
import { logger } from '../../logging/logger'
|
||||
import { claudeSessionFileExists, codexSessionFileExists } from '../../sessions/sessionFiles'
|
||||
@@ -27,6 +25,7 @@ import { makeNonce } from '../../webview/html'
|
||||
import { decideAutoReviewPath } from '../autoReviewDecision'
|
||||
import { pickBrainstormContinuePath } from '../brainstormContinue'
|
||||
import { cleanupFeatureBranch } from './branchCleanup'
|
||||
import { forgeOrErrorMessage } from './forgeGate'
|
||||
import { planResumeCwd, resumeTerminalName } from './resumeTarget'
|
||||
import { cancelReviewSessionCapture, prepareReviewSessionCapture, recoverReviewSessionCapture } from './reviewSessionCapture'
|
||||
import { submitToSessionTerminal } from './terminals'
|
||||
@@ -632,21 +631,10 @@ export async function handleImplement(
|
||||
message: `开始实施 #${issueNumber}`,
|
||||
details: `planFile=${planFile}`,
|
||||
})
|
||||
const workspaceRoot = workspace.workspaceFolders?.[0]?.uri.fsPath
|
||||
if (!workspaceRoot) {
|
||||
void window.showErrorMessage('请先打开一个工作区文件夹')
|
||||
const forge = await forgeOrErrorMessage(panel)
|
||||
if (!forge)
|
||||
return
|
||||
}
|
||||
const remote = await detectRepo(workspaceRoot)
|
||||
if (!remote) {
|
||||
void window.showErrorMessage('当前工作区没有 Gitea 远程仓库')
|
||||
return
|
||||
}
|
||||
const token = await getToken(panel.context, remote.host)
|
||||
if (!token) {
|
||||
void window.showErrorMessage('请先完成 Gitea 配置')
|
||||
return
|
||||
}
|
||||
const { workspaceRoot } = forge
|
||||
|
||||
// worktree 目录名用 spec/plan 的 slug(可读、与 spec 目录同名),不再用 planFile 的 hash。
|
||||
const slug = deriveSlug(planFile)
|
||||
@@ -686,8 +674,7 @@ export async function handleImplement(
|
||||
branch,
|
||||
issueNumber,
|
||||
devBranch: settings.devBranch,
|
||||
remote,
|
||||
token,
|
||||
forge,
|
||||
})
|
||||
// worktree 现在建在工作区外(~/Sources/worktree/<项目>/...),父目录可能不存在;
|
||||
// git worktree add 不会创建多层父目录,先补齐。
|
||||
@@ -711,17 +698,10 @@ export async function handleImplement(
|
||||
// 实施——门禁侧会退回本地终端兜底。
|
||||
void (async () => {
|
||||
try {
|
||||
const me = await loginForToken(remote.host, token)
|
||||
const me = await loginFor(forge)
|
||||
if (!me)
|
||||
return
|
||||
await updateIssueAssignees({
|
||||
host: remote.host,
|
||||
token,
|
||||
owner: remote.owner,
|
||||
repo: remote.repo,
|
||||
index: issueNumber,
|
||||
assignees: [me],
|
||||
})
|
||||
await forge.provider.setIssueAssignees(issueNumber, [me])
|
||||
logger.add({
|
||||
level: 'info',
|
||||
source: 'implement',
|
||||
@@ -887,21 +867,10 @@ export async function handleImplement(
|
||||
* 全程在 workspaceRoot,不建 worktree / 分支(这是讨论,不是编码)。
|
||||
*/
|
||||
export async function handleContinueBrainstorm(panel: KanbanWebviewPanel, issueNumber: number): Promise<void> {
|
||||
const workspaceRoot = workspace.workspaceFolders?.[0]?.uri.fsPath
|
||||
if (!workspaceRoot) {
|
||||
void window.showErrorMessage('请先打开一个工作区文件夹')
|
||||
const forge = await forgeOrErrorMessage(panel)
|
||||
if (!forge)
|
||||
return
|
||||
}
|
||||
const remote = await detectRepo(workspaceRoot)
|
||||
if (!remote) {
|
||||
void window.showErrorMessage('当前工作区没有 Gitea 远程仓库')
|
||||
return
|
||||
}
|
||||
const token = await getToken(panel.context, remote.host)
|
||||
if (!token) {
|
||||
void window.showErrorMessage('请先完成 Gitea 配置')
|
||||
return
|
||||
}
|
||||
const { workspaceRoot } = forge
|
||||
|
||||
// state 读一次就够:profile / 会话 id 和提示词上下文(原型插件、会话 id 等)
|
||||
// 都从这份里取。读不到就按空状态走,提示词里的条件块自然走 else 分支。
|
||||
@@ -1049,21 +1018,10 @@ export async function handleStartTestSession(panel: KanbanWebviewPanel, issueNum
|
||||
return
|
||||
}
|
||||
|
||||
const workspaceRoot = workspace.workspaceFolders?.[0]?.uri.fsPath
|
||||
if (!workspaceRoot) {
|
||||
void window.showErrorMessage('请先打开一个工作区文件夹')
|
||||
const forge = await forgeOrErrorMessage(panel)
|
||||
if (!forge)
|
||||
return
|
||||
}
|
||||
const remote = await detectRepo(workspaceRoot)
|
||||
if (!remote) {
|
||||
void window.showErrorMessage('当前工作区没有 Gitea 远程仓库')
|
||||
return
|
||||
}
|
||||
const token = await getToken(panel.context, remote.host)
|
||||
if (!token) {
|
||||
void window.showErrorMessage('请先完成 Gitea 配置')
|
||||
return
|
||||
}
|
||||
const { workspaceRoot } = forge
|
||||
|
||||
// 从 state JSON 读 pr / testProfile / worktree。pr 仅在主 worktree(已合并)
|
||||
// 测试时必需——那条路径要靠 tea 了解合并进来的改动;worktree 内测试代码
|
||||
@@ -1254,9 +1212,7 @@ export async function handleResumeTestSession(panel: KanbanWebviewPanel, session
|
||||
let branchForHook = ''
|
||||
let testProfilePath: string | undefined
|
||||
if (workspaceRoot) {
|
||||
const remote = await detectRepo(workspaceRoot)
|
||||
const token = remote ? await getToken(panel.context, remote.host) : undefined
|
||||
if (remote && token) {
|
||||
if ((await resolveForge(panel.context)).ok) {
|
||||
try {
|
||||
const stateObj = await panel.readIssueState(issueNumber)
|
||||
const worktreePathRel = typeof stateObj?.worktreePath === 'string' ? stateObj.worktreePath : undefined
|
||||
@@ -1366,21 +1322,10 @@ export async function handleStartPrototypeSession(panel: KanbanWebviewPanel, iss
|
||||
return
|
||||
}
|
||||
|
||||
const workspaceRoot = workspace.workspaceFolders?.[0]?.uri.fsPath
|
||||
if (!workspaceRoot) {
|
||||
void window.showErrorMessage('请先打开一个工作区文件夹')
|
||||
const forge = await forgeOrErrorMessage(panel)
|
||||
if (!forge)
|
||||
return
|
||||
}
|
||||
const remote = await detectRepo(workspaceRoot)
|
||||
if (!remote) {
|
||||
void window.showErrorMessage('当前工作区没有 Gitea 远程仓库')
|
||||
return
|
||||
}
|
||||
const token = await getToken(panel.context, remote.host)
|
||||
if (!token) {
|
||||
void window.showErrorMessage('请先完成 Gitea 配置')
|
||||
return
|
||||
}
|
||||
const { workspaceRoot } = forge
|
||||
|
||||
const lockCheck = await panel.resolveLockedReason(issueNumber)
|
||||
if (lockCheck.locked) {
|
||||
@@ -1516,9 +1461,7 @@ export async function handleResumePrototypeSession(panel: KanbanWebviewPanel, se
|
||||
let branchForHook = ''
|
||||
let prototypeProfilePath: string | undefined
|
||||
if (workspaceRoot) {
|
||||
const remote = await detectRepo(workspaceRoot)
|
||||
const token = remote ? await getToken(panel.context, remote.host) : undefined
|
||||
if (remote && token) {
|
||||
if ((await resolveForge(panel.context)).ok) {
|
||||
try {
|
||||
const stateObj = await panel.readIssueState(issueNumber)
|
||||
const worktreePathRel = typeof stateObj?.worktreePath === 'string' ? stateObj.worktreePath : undefined
|
||||
|
||||
@@ -2,7 +2,7 @@ import type {
|
||||
Terminal,
|
||||
TerminalEditorLocationOptions,
|
||||
} from 'vscode'
|
||||
import type { Issue } from '../../gitea/types'
|
||||
import type { Issue } from '../../issues/types'
|
||||
import type { KanbanWebviewPanel } from '../KanbanPanel'
|
||||
import { TabInputTerminal, window } from 'vscode'
|
||||
import { logger } from '../../logging/logger'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { IssueColumn } from '../../gitea/types'
|
||||
import type { IssueColumn } from '../../issues/types'
|
||||
|
||||
export function canResetToTodo(fromColumn: IssueColumn | undefined): boolean {
|
||||
return fromColumn === 'in-progress' || fromColumn === 'review'
|
||||
|
||||
@@ -2,8 +2,7 @@ import type { HookContext } from '../../git/worktreeHooks'
|
||||
import type { KanbanWebviewPanel } from '../KanbanPanel'
|
||||
import { execFile } from 'node:child_process'
|
||||
import { commands, Uri, window, workspace } from 'vscode'
|
||||
import { getToken } from '../../auth/secrets'
|
||||
import { detectRepo } from '../../git/remote'
|
||||
import { resolveForge } from '../../forge/context'
|
||||
import { expandTilde, killProcessesUsingWorktree, removeWorktreeDir, resolveWorktreePath } from '../../git/worktree'
|
||||
import {
|
||||
runImplTabPostCloseHook,
|
||||
@@ -130,17 +129,15 @@ export async function handleDeleteWorktree(panel: KanbanWebviewPanel, issueNumbe
|
||||
console.warn('[superpowers] failed to clear worktree state JSON:', err)
|
||||
}
|
||||
|
||||
// 连同 feature 分支一起删(本地 + 远程,best-effort)。受保护分支跳过、缺 token 只删本地,
|
||||
// 连同 feature 分支一起删(本地 + 远程,best-effort)。受保护分支跳过、解析不出代码托管仓库只删本地,
|
||||
// 逻辑与「拖到完成」共用同一个 cleanupFeatureBranch。
|
||||
const remote = await detectRepo(workspaceRoot)
|
||||
const token = remote ? await getToken(panel.context, remote.host) : undefined
|
||||
const resolved = await resolveForge(panel.context)
|
||||
await cleanupFeatureBranch({
|
||||
workspaceRoot,
|
||||
branch,
|
||||
issueNumber,
|
||||
devBranch: settingsForHook.devBranch,
|
||||
remote: remote ? { host: remote.host, owner: remote.owner, repo: remote.repo } : undefined,
|
||||
token,
|
||||
forge: resolved.ok ? resolved.forge : undefined,
|
||||
})
|
||||
|
||||
// null 才能过 postMessage 序列化;undefined 会被丢掉,webview 合并不清字段
|
||||
@@ -299,15 +296,10 @@ export async function dispatchWorktreeHook(
|
||||
*/
|
||||
export async function dispatchImplTabPostCloseAsync(panel: KanbanWebviewPanel, issueNumber: number): Promise<void> {
|
||||
try {
|
||||
const workspaceRoot = workspace.workspaceFolders?.[0]?.uri.fsPath
|
||||
if (!workspaceRoot)
|
||||
return
|
||||
const remote = await detectRepo(workspaceRoot)
|
||||
if (!remote)
|
||||
return
|
||||
const token = await getToken(panel.context, remote.host)
|
||||
if (!token)
|
||||
const resolved = await resolveForge(panel.context)
|
||||
if (!resolved.ok)
|
||||
return
|
||||
const { workspaceRoot } = resolved.forge
|
||||
const stateObj = await panel.readIssueState(issueNumber)
|
||||
const branch = typeof stateObj.branch === 'string' ? stateObj.branch : ''
|
||||
const wt = typeof stateObj.worktreePath === 'string' ? stateObj.worktreePath : ''
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* only write-back the user asked for.
|
||||
*/
|
||||
|
||||
import type { IssueColumn } from '../../gitea/types'
|
||||
import type { IssueColumn } from '../../issues/types'
|
||||
import type { YouTrackAuth } from '../../youtrack/api'
|
||||
import type { KanbanWebviewPanel } from '../KanbanPanel'
|
||||
import { ThemeIcon, window, workspace } from 'vscode'
|
||||
|
||||
Reference in new issue
Block a user