✨ feat(vscode): 「改动」tab 重做为整 PR 聚合的主从式内联 diff(git-diff-view)
- 左树右 diff:左栏全 PR 文件树 + 统计条(共改 N · +X−Y · 已查看 M/N),右栏选中单文件内联 diff - 用 @git-diff-view/react 只渲染当前选中文件,绕开上万行全量渲染的性能坑 - gitea 新增 listPullRequestFiles、getPullRequest 取 merge_base/head,按需回两版原文 - diff 走 merge_base→head 三点口径(与 Gitea 网页一致),getRawFile 404 兜底成全增/全删/改名 - 已查看态按 head sha 落盘、新 push 自然重置;去掉提交列表、原生 diff 与 spx-gitea 虚拟文档 provider
This commit is contained in:
@@ -26,7 +26,7 @@ import { webhookCoordinator } from '../webhook/coordinator'
|
||||
import { loadYouTrackIssues } from '../youtrack/issueLoader'
|
||||
import * as issues from './handlers/issues'
|
||||
import * as managedSessions from './handlers/managedSessions'
|
||||
import * as prCommits from './handlers/prCommits'
|
||||
import * as prFiles from './handlers/prFiles'
|
||||
import * as sessions from './handlers/sessions'
|
||||
import * as settings from './handlers/settings'
|
||||
import * as terminals from './handlers/terminals'
|
||||
@@ -546,30 +546,20 @@ export class KanbanWebviewPanel {
|
||||
managedSessions.handleManagedSessionsCloseTab(this, msg.sessionId)
|
||||
return
|
||||
}
|
||||
if (msg.type === 'pr-commits/get') {
|
||||
void prCommits.handleGetPrCommits(this, msg.issueNumber)
|
||||
if (msg.type === 'pr-files/get') {
|
||||
void prFiles.handleGetPrFiles(this, msg.issueNumber)
|
||||
return
|
||||
}
|
||||
if (msg.type === 'pr-commit-files/get') {
|
||||
void prCommits.handleGetPrCommitFiles(this, msg.issueNumber, msg.sha)
|
||||
return
|
||||
}
|
||||
if (msg.type === 'pr-commit-diff/open') {
|
||||
void prCommits.handleOpenPrCommitDiff(this, {
|
||||
if (msg.type === 'pr-file-diff/get') {
|
||||
void prFiles.handleGetPrFileDiff(this, {
|
||||
issueNumber: msg.issueNumber,
|
||||
sha: msg.sha,
|
||||
parentSha: msg.parentSha,
|
||||
path: msg.path,
|
||||
status: msg.status,
|
||||
previousPath: msg.previousPath,
|
||||
})
|
||||
return
|
||||
}
|
||||
if (msg.type === 'pr-review/set') {
|
||||
void prCommits.handleSetPrReviewConfirmed(this, msg)
|
||||
return
|
||||
}
|
||||
if (msg.type === 'pr-review/set-commits') {
|
||||
void prCommits.handleSetPrReviewConfirmedCommits(this, msg)
|
||||
void prFiles.handleSetPrReviewConfirmed(this, msg)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,265 +0,0 @@
|
||||
/**
|
||||
* 「提交」tab 的扩展侧 handler:实时从 Gitea 读 PR 提交、提交内文件,并用
|
||||
* VS Code 原生 diff 比较改动前后。
|
||||
*
|
||||
* 数据全程实时拉取,不落任何 state JSON。文件内容通过一个
|
||||
* TextDocumentContentProvider(scheme `spx-gitea`)按需取 raw,使 diff 的两侧
|
||||
* 各自指向一个 ref 下的文件。
|
||||
*/
|
||||
|
||||
import type { KanbanWebviewPanel } from '../KanbanPanel'
|
||||
import { commands, Uri, window, workspace, type TextDocumentContentProvider } from 'vscode'
|
||||
import { getToken } from '../../auth/secrets'
|
||||
import { detectRepo } from '../../git/remote'
|
||||
import { getGitCommit, getRawFile, listPullRequestCommits } from '../../gitea/api'
|
||||
import { readStateJsonComment } from '../../gitea/stateJson'
|
||||
import { readConfirmed, readConfirmedCommits, writeConfirmed, writeConfirmedCommits } from '../../sessions/prReviewStore'
|
||||
|
||||
/** diff 文件内容走的虚拟文档 scheme。 */
|
||||
const SPX_GITEA_SCHEME = 'spx-gitea'
|
||||
|
||||
/** provider 只需注册一次;模块级守卫避免重复注册同一 scheme 抛错。 */
|
||||
let providerRegistered = false
|
||||
|
||||
/**
|
||||
* 解析当前工作区的 Gitea 仓库与 token。三者任一缺失返回 undefined,
|
||||
* 调用方据此走空态/错误分支。
|
||||
*/
|
||||
async function resolveRepoContext(panel: KanbanWebviewPanel): Promise<
|
||||
{ host: string, owner: string, repo: string, token: string } | undefined
|
||||
> {
|
||||
const workspaceRoot = workspace.workspaceFolders?.[0]?.uri.fsPath
|
||||
if (!workspaceRoot)
|
||||
return undefined
|
||||
const remote = await detectRepo(workspaceRoot)
|
||||
if (!remote)
|
||||
return undefined
|
||||
const token = await getToken(panel.context, remote.host)
|
||||
if (!token)
|
||||
return undefined
|
||||
return { host: remote.host, owner: remote.owner, repo: remote.repo, token }
|
||||
}
|
||||
|
||||
/**
|
||||
* 拉取工单对应 PR 的提交列表并推给 webview。无 PR 时回空列表(前端按空态处理,
|
||||
* 不算错误);缺工作区/远程/token 同样回空列表。真出错才带 error。
|
||||
*/
|
||||
export async function handleGetPrCommits(panel: KanbanWebviewPanel, issueNumber: number): Promise<void> {
|
||||
try {
|
||||
const ctx = await resolveRepoContext(panel)
|
||||
if (!ctx) {
|
||||
panel.postMessage({ type: 'pr-commits/show', issueNumber, commits: [], confirmedCommits: [] })
|
||||
return
|
||||
}
|
||||
|
||||
const stateObj = await readStateJsonComment({
|
||||
host: ctx.host,
|
||||
owner: ctx.owner,
|
||||
repo: ctx.repo,
|
||||
token: ctx.token,
|
||||
issueNumber,
|
||||
})
|
||||
const pr = typeof stateObj?.pr === 'string' && stateObj.pr.length > 0 ? stateObj.pr : undefined
|
||||
if (!pr) {
|
||||
panel.postMessage({ type: 'pr-commits/show', issueNumber, commits: [], confirmedCommits: [] })
|
||||
return
|
||||
}
|
||||
|
||||
const commits = await listPullRequestCommits({
|
||||
host: ctx.host,
|
||||
owner: ctx.owner,
|
||||
repo: ctx.repo,
|
||||
token: ctx.token,
|
||||
index: Number(pr),
|
||||
})
|
||||
const workspaceRoot = workspace.workspaceFolders?.[0]?.uri.fsPath
|
||||
const confirmedCommits = workspaceRoot
|
||||
? await readConfirmedCommits(workspaceRoot, issueNumber)
|
||||
: []
|
||||
panel.postMessage({ type: 'pr-commits/show', issueNumber, commits, confirmedCommits })
|
||||
}
|
||||
catch (err) {
|
||||
panel.postMessage({
|
||||
type: 'pr-commits/show',
|
||||
issueNumber,
|
||||
commits: [],
|
||||
confirmedCommits: [],
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取某提交的文件清单并推给 webview。parentSha 一并回传,供前端发起 diff 时
|
||||
* 作为左侧 ref。
|
||||
*/
|
||||
export async function handleGetPrCommitFiles(
|
||||
panel: KanbanWebviewPanel,
|
||||
issueNumber: number,
|
||||
sha: string,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const ctx = await resolveRepoContext(panel)
|
||||
if (!ctx) {
|
||||
panel.postMessage({ type: 'pr-commit-files/show', issueNumber, sha, files: [], confirmed: [] })
|
||||
return
|
||||
}
|
||||
|
||||
const detail = await getGitCommit({
|
||||
host: ctx.host,
|
||||
owner: ctx.owner,
|
||||
repo: ctx.repo,
|
||||
token: ctx.token,
|
||||
sha,
|
||||
})
|
||||
const workspaceRoot = workspace.workspaceFolders?.[0]?.uri.fsPath
|
||||
const confirmed = workspaceRoot
|
||||
? await readConfirmed(workspaceRoot, issueNumber, sha)
|
||||
: []
|
||||
panel.postMessage({
|
||||
type: 'pr-commit-files/show',
|
||||
issueNumber,
|
||||
sha,
|
||||
parentSha: detail.parentSha,
|
||||
files: detail.files,
|
||||
confirmed,
|
||||
})
|
||||
}
|
||||
catch (err) {
|
||||
panel.postMessage({
|
||||
type: 'pr-commit-files/show',
|
||||
issueNumber,
|
||||
sha,
|
||||
files: [],
|
||||
confirmed: [],
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 持久化某提交里已确认(已审阅)的文件路径集合到 `.spx/pr-review-confirmed.json`。
|
||||
* 拿不到工作区根则静默跳过;写盘失败兜底打日志、不抛——确认态丢失不该中断 UI。
|
||||
*/
|
||||
export async function handleSetPrReviewConfirmed(
|
||||
panel: KanbanWebviewPanel,
|
||||
args: { issueNumber: number, sha: string, confirmed: string[] },
|
||||
): Promise<void> {
|
||||
try {
|
||||
const workspaceRoot = workspace.workspaceFolders?.[0]?.uri.fsPath
|
||||
if (!workspaceRoot)
|
||||
return
|
||||
await writeConfirmed(workspaceRoot, args.issueNumber, args.sha, args.confirmed)
|
||||
}
|
||||
catch (err) {
|
||||
console.warn('handleSetPrReviewConfirmed 写盘失败', err)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 持久化某工单已确认(已审阅)的提交 sha 集合到 `.spx/pr-review-confirmed.json`。
|
||||
* 拿不到工作区根则静默跳过;写盘失败兜底打日志、不抛——确认态丢失不该中断 UI。
|
||||
*/
|
||||
export async function handleSetPrReviewConfirmedCommits(
|
||||
panel: KanbanWebviewPanel,
|
||||
args: { issueNumber: number, confirmed: string[] },
|
||||
): Promise<void> {
|
||||
try {
|
||||
const workspaceRoot = workspace.workspaceFolders?.[0]?.uri.fsPath
|
||||
if (!workspaceRoot)
|
||||
return
|
||||
await writeConfirmedCommits(workspaceRoot, args.issueNumber, args.confirmed)
|
||||
}
|
||||
catch (err) {
|
||||
console.warn('handleSetPrReviewConfirmedCommits 写盘失败', err)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 懒注册 raw 内容 provider(只一次)。闭包捕获 panel 以便取 token——diff 两侧的
|
||||
* 文档内容都经此 provider 按 uri 里的 ref 拉取。
|
||||
*/
|
||||
function ensureContentProvider(panel: KanbanWebviewPanel): void {
|
||||
if (providerRegistered)
|
||||
return
|
||||
providerRegistered = true
|
||||
|
||||
const provider: TextDocumentContentProvider = {
|
||||
async provideTextDocumentContent(uri: Uri): Promise<string> {
|
||||
const params = new URLSearchParams(uri.query)
|
||||
const host = params.get('host') ?? ''
|
||||
const owner = params.get('owner') ?? ''
|
||||
const repo = params.get('repo') ?? ''
|
||||
const ref = params.get('ref') ?? ''
|
||||
// uri.path 形如 `/src/foo.ts`:去掉前导斜杠并 decode 还原原始路径。
|
||||
const filepath = decodeURIComponent(uri.path.replace(/^\//, ''))
|
||||
if (!host || !owner || !repo)
|
||||
return ''
|
||||
const token = await getToken(panel.context, host)
|
||||
if (!token)
|
||||
return ''
|
||||
return getRawFile({ host, owner, repo, token, filepath, ref })
|
||||
},
|
||||
}
|
||||
|
||||
const disposable = workspace.registerTextDocumentContentProvider(SPX_GITEA_SCHEME, provider)
|
||||
panel.context.subscriptions.push(disposable)
|
||||
}
|
||||
|
||||
/** 把改动文件的一侧(某 ref 下内容)编码成 spx-gitea 虚拟文档 uri。 */
|
||||
function buildSideUri(opts: {
|
||||
host: string
|
||||
owner: string
|
||||
repo: string
|
||||
ref: string
|
||||
path: string
|
||||
}): Uri {
|
||||
const query = new URLSearchParams({
|
||||
host: opts.host,
|
||||
owner: opts.owner,
|
||||
repo: opts.repo,
|
||||
ref: opts.ref,
|
||||
}).toString()
|
||||
return Uri.from({ scheme: SPX_GITEA_SCHEME, path: `/${opts.path}`, query })
|
||||
}
|
||||
|
||||
/**
|
||||
* 用 VS Code 原生 diff 比较一个文件在 parentSha → sha 之间的改动。
|
||||
*
|
||||
* 左侧 = parentSha 下内容,右侧 = sha 下内容。根提交无 parentSha 时左侧 ref 给
|
||||
* 空串,raw 取不到(404)→ 空内容,自然呈现全新增。删除文件右侧在 sha 下 404
|
||||
* → 空,呈现删除。
|
||||
*/
|
||||
export async function handleOpenPrCommitDiff(
|
||||
panel: KanbanWebviewPanel,
|
||||
args: { issueNumber: number, sha: string, parentSha?: string, path: string, status: string },
|
||||
): Promise<void> {
|
||||
try {
|
||||
const ctx = await resolveRepoContext(panel)
|
||||
if (!ctx) {
|
||||
void window.showWarningMessage('当前工作区没有 Gitea 仓库或未配置 token')
|
||||
return
|
||||
}
|
||||
ensureContentProvider(panel)
|
||||
|
||||
const leftUri = buildSideUri({
|
||||
host: ctx.host,
|
||||
owner: ctx.owner,
|
||||
repo: ctx.repo,
|
||||
ref: args.parentSha ?? '',
|
||||
path: args.path,
|
||||
})
|
||||
const rightUri = buildSideUri({
|
||||
host: ctx.host,
|
||||
owner: ctx.owner,
|
||||
repo: ctx.repo,
|
||||
ref: args.sha,
|
||||
path: args.path,
|
||||
})
|
||||
const title = `${args.path} (${(args.parentSha ?? '∅').slice(0, 7)} ↔ ${args.sha.slice(0, 7)})`
|
||||
await commands.executeCommand('vscode.diff', leftUri, rightUri, title)
|
||||
}
|
||||
catch (err) {
|
||||
void window.showErrorMessage(`打开 diff 失败:${err instanceof Error ? err.message : String(err)}`)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
/**
|
||||
* 「改动」tab 的扩展侧 handler:实时从 Gitea 读整个 PR 的改动文件清单,并按需取
|
||||
* 单个文件改动前后的原文,交给 webview 内联渲染(主从式 diff)。
|
||||
*
|
||||
* 数据全程实时拉取,只把「已查看」确认态落到 `.spx/pr-review-confirmed.json`
|
||||
* (键含 head sha,新 push 自然重置)。diff 左右两侧用 merge_base → head 的原文,
|
||||
* 与 Gitea 网页 diff 口径一致;getRawFile 对 404 兜底空串,故新增/删除/改名天然成立。
|
||||
*/
|
||||
|
||||
import type { KanbanWebviewPanel } from '../KanbanPanel'
|
||||
import { workspace } from 'vscode'
|
||||
import { getToken } from '../../auth/secrets'
|
||||
import { detectRepo } from '../../git/remote'
|
||||
import { getPullRequest, getRawFile, listPullRequestFiles } from '../../gitea/api'
|
||||
import { readStateJsonComment } from '../../gitea/stateJson'
|
||||
import { readConfirmed, writeConfirmed } from '../../sessions/prReviewStore'
|
||||
|
||||
/**
|
||||
* 解析当前工作区的 Gitea 仓库与 token。三者任一缺失返回 undefined,
|
||||
* 调用方据此走空态/错误分支。
|
||||
*/
|
||||
async function resolveRepoContext(panel: KanbanWebviewPanel): Promise<
|
||||
{ host: string, owner: string, repo: string, token: string } | undefined
|
||||
> {
|
||||
const workspaceRoot = workspace.workspaceFolders?.[0]?.uri.fsPath
|
||||
if (!workspaceRoot)
|
||||
return undefined
|
||||
const remote = await detectRepo(workspaceRoot)
|
||||
if (!remote)
|
||||
return undefined
|
||||
const token = await getToken(panel.context, remote.host)
|
||||
if (!token)
|
||||
return undefined
|
||||
return { host: remote.host, owner: remote.owner, repo: remote.repo, token }
|
||||
}
|
||||
|
||||
/** 从工单的 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,
|
||||
})
|
||||
const pr = typeof stateObj?.pr === 'string' && stateObj.pr.length > 0 ? stateObj.pr : undefined
|
||||
return pr ? Number(pr) : undefined
|
||||
}
|
||||
|
||||
/** 由文件扩展名推断高亮语言 id(highlight.js/lowlight 口径);未知则回退扩展名本身。 */
|
||||
function langFromPath(path: string): string {
|
||||
const ext = path.split('.').pop()?.toLowerCase() ?? ''
|
||||
const map: Record<string, string> = {
|
||||
ts: 'typescript',
|
||||
tsx: 'typescript',
|
||||
js: 'javascript',
|
||||
jsx: 'javascript',
|
||||
mjs: 'javascript',
|
||||
cjs: 'javascript',
|
||||
json: 'json',
|
||||
py: 'python',
|
||||
go: 'go',
|
||||
rs: 'rust',
|
||||
java: 'java',
|
||||
kt: 'kotlin',
|
||||
kts: 'kotlin',
|
||||
rb: 'ruby',
|
||||
php: 'php',
|
||||
c: 'c',
|
||||
h: 'c',
|
||||
cpp: 'cpp',
|
||||
cc: 'cpp',
|
||||
hpp: 'cpp',
|
||||
cs: 'csharp',
|
||||
swift: 'swift',
|
||||
md: 'markdown',
|
||||
markdown: 'markdown',
|
||||
yml: 'yaml',
|
||||
yaml: 'yaml',
|
||||
toml: 'toml',
|
||||
ini: 'ini',
|
||||
sh: 'bash',
|
||||
bash: 'bash',
|
||||
zsh: 'bash',
|
||||
sql: 'sql',
|
||||
css: 'css',
|
||||
scss: 'scss',
|
||||
less: 'less',
|
||||
html: 'xml',
|
||||
htm: 'xml',
|
||||
xml: 'xml',
|
||||
vue: 'xml',
|
||||
svelte: 'xml',
|
||||
}
|
||||
return map[ext] ?? ext
|
||||
}
|
||||
|
||||
/**
|
||||
* 拉取工单对应 PR 的全部改动文件清单 + head/mergeBase ref + 已确认集合,推给 webview。
|
||||
* 无 PR/缺上下文回空(前端按空态处理);真出错才带 error。
|
||||
*/
|
||||
export async function handleGetPrFiles(panel: KanbanWebviewPanel, issueNumber: number): Promise<void> {
|
||||
try {
|
||||
const ctx = await resolveRepoContext(panel)
|
||||
if (!ctx) {
|
||||
panel.postMessage({ type: 'pr-files/show', issueNumber, headSha: '', mergeBase: '', files: [], confirmed: [] })
|
||||
return
|
||||
}
|
||||
|
||||
const index = await resolvePrNumber(ctx, issueNumber)
|
||||
if (index === undefined) {
|
||||
panel.postMessage({ type: 'pr-files/show', issueNumber, headSha: '', mergeBase: '', files: [], confirmed: [] })
|
||||
return
|
||||
}
|
||||
|
||||
const pr = await getPullRequest({ host: ctx.host, owner: ctx.owner, repo: ctx.repo, token: ctx.token, index })
|
||||
// 三点 diff 的左侧 ref:优先 merge_base,缺失则退回 base sha。
|
||||
const mergeBase = pr.mergeBase || pr.baseSha
|
||||
const files = await listPullRequestFiles({
|
||||
host: ctx.host,
|
||||
owner: ctx.owner,
|
||||
repo: ctx.repo,
|
||||
token: ctx.token,
|
||||
index,
|
||||
})
|
||||
const workspaceRoot = workspace.workspaceFolders?.[0]?.uri.fsPath
|
||||
const confirmed = workspaceRoot
|
||||
? await readConfirmed(workspaceRoot, issueNumber, pr.headSha)
|
||||
: []
|
||||
panel.postMessage({
|
||||
type: 'pr-files/show',
|
||||
issueNumber,
|
||||
headSha: pr.headSha,
|
||||
mergeBase,
|
||||
files,
|
||||
confirmed,
|
||||
})
|
||||
}
|
||||
catch (err) {
|
||||
panel.postMessage({
|
||||
type: 'pr-files/show',
|
||||
issueNumber,
|
||||
headSha: '',
|
||||
mergeBase: '',
|
||||
files: [],
|
||||
confirmed: [],
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取单个文件改动前后的原文:左 = previousPath@mergeBase(无改名则 path),
|
||||
* 右 = path@head。两侧原文 + 语言推断回给 webview,由 git-diff-view 内联渲染。
|
||||
*/
|
||||
export async function handleGetPrFileDiff(
|
||||
panel: KanbanWebviewPanel,
|
||||
args: { issueNumber: number, path: string, previousPath?: string },
|
||||
): Promise<void> {
|
||||
try {
|
||||
const ctx = await resolveRepoContext(panel)
|
||||
if (!ctx) {
|
||||
panel.postMessage({ type: 'pr-file-diff/show', issueNumber: args.issueNumber, path: args.path, oldContent: '', newContent: '' })
|
||||
return
|
||||
}
|
||||
const index = await resolvePrNumber(ctx, 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 }),
|
||||
])
|
||||
panel.postMessage({
|
||||
type: 'pr-file-diff/show',
|
||||
issueNumber: args.issueNumber,
|
||||
path: args.path,
|
||||
oldContent,
|
||||
newContent,
|
||||
oldLang: langFromPath(oldPath),
|
||||
newLang: langFromPath(args.path),
|
||||
})
|
||||
}
|
||||
catch (err) {
|
||||
panel.postMessage({
|
||||
type: 'pr-file-diff/show',
|
||||
issueNumber: args.issueNumber,
|
||||
path: args.path,
|
||||
oldContent: '',
|
||||
newContent: '',
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 持久化某 PR 里已确认(已审阅)的文件路径集合到 `.spx/pr-review-confirmed.json`,
|
||||
* 键含 head sha。拿不到工作区根则静默跳过;写盘失败兜底打日志、不抛——确认态丢失
|
||||
* 不该中断 UI。
|
||||
*/
|
||||
export async function handleSetPrReviewConfirmed(
|
||||
panel: KanbanWebviewPanel,
|
||||
args: { issueNumber: number, headSha: string, confirmed: string[] },
|
||||
): Promise<void> {
|
||||
try {
|
||||
const workspaceRoot = workspace.workspaceFolders?.[0]?.uri.fsPath
|
||||
if (!workspaceRoot)
|
||||
return
|
||||
await writeConfirmed(workspaceRoot, args.issueNumber, args.headSha, args.confirmed)
|
||||
}
|
||||
catch (err) {
|
||||
console.warn('handleSetPrReviewConfirmed 写盘失败', err)
|
||||
}
|
||||
}
|
||||
@@ -19,18 +19,16 @@ export interface ManagedSessionsShowData {
|
||||
sessions: ManagedSessionShowItem[]
|
||||
}
|
||||
|
||||
/** PR 提交的精简视图(webview 列表渲染用)。 */
|
||||
export interface PrCommit {
|
||||
sha: string
|
||||
message: string
|
||||
authorName: string
|
||||
date: string
|
||||
}
|
||||
|
||||
/** 提交内单个文件改动:status 取 added/modified/deleted/renamed/copied 等。 */
|
||||
export interface PrCommitFile {
|
||||
/**
|
||||
* PR 改动里的一个文件:status 取 added/modified/deleted/renamed/copied 等,
|
||||
* additions/deletions 求和成统计条,previousFilename 在改名时给出旧路径。
|
||||
*/
|
||||
export interface PrFile {
|
||||
path: string
|
||||
status: string
|
||||
additions: number
|
||||
deletions: number
|
||||
previousFilename?: string
|
||||
}
|
||||
|
||||
export interface ToastLink {
|
||||
@@ -102,8 +100,8 @@ export type ExtensionToWebview
|
||||
| { type: 'issue/remove', issueNumber: number }
|
||||
| { type: 'profiles/show', data: ProfilesData }
|
||||
| { type: 'managed-sessions/show', data: ManagedSessionsShowData }
|
||||
| { type: 'pr-commits/show', issueNumber: number, commits: PrCommit[], confirmedCommits: string[], error?: string }
|
||||
| { type: 'pr-commit-files/show', issueNumber: number, sha: string, parentSha?: string, files: PrCommitFile[], confirmed: string[], error?: string }
|
||||
| { type: 'pr-files/show', issueNumber: number, headSha: string, mergeBase: string, files: PrFile[], confirmed: string[], error?: string }
|
||||
| { type: 'pr-file-diff/show', issueNumber: number, path: string, oldContent: string, newContent: string, oldLang?: string, newLang?: string, error?: string }
|
||||
|
||||
export type WebviewToExtension
|
||||
= | { type: 'issues/refresh' }
|
||||
@@ -174,8 +172,6 @@ export type WebviewToExtension
|
||||
| { type: 'managed-sessions/resume', sessionId: string }
|
||||
| { type: 'managed-sessions/delete', sessionId: string }
|
||||
| { type: 'managed-sessions/close-tab', sessionId: string }
|
||||
| { type: 'pr-commits/get', issueNumber: number }
|
||||
| { type: 'pr-commit-files/get', issueNumber: number, sha: string }
|
||||
| { type: 'pr-commit-diff/open', issueNumber: number, sha: string, parentSha?: string, path: string, status: string }
|
||||
| { type: 'pr-review/set', issueNumber: number, sha: string, confirmed: string[] }
|
||||
| { type: 'pr-review/set-commits', issueNumber: number, confirmed: string[] }
|
||||
| { type: 'pr-files/get', issueNumber: number }
|
||||
| { type: 'pr-file-diff/get', issueNumber: number, path: string, previousPath?: string }
|
||||
| { type: 'pr-review/set', issueNumber: number, headSha: string, confirmed: string[] }
|
||||
|
||||
Reference in New Issue
Block a user