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:
2026-06-29 10:52:36 +08:00
parent ceea62ed16
commit fb505d59c5
18 changed files with 1149 additions and 1127 deletions
-141
View File
@@ -1,141 +0,0 @@
/**
* 「提交」tab 的订阅 hook:按工单号拉取 PR 提交、按需拉提交内文件,并触发原生
* diff。数据全程实时从扩展侧(Gitea API)取,本 hook 只做缓存与订阅。
*
* - issueNumber 变化(含首次)时 postMessage `pr-commits/get`,并清空上一工单的
* 文件缓存与 parentSha 映射,避免串台。
* - 订阅 `pr-commits/show` / `pr-commit-files/show`,只认 issueNumber 匹配的消息
* (扩展侧可能并发多工单的回包)。
* - filesBySha 缓存:同一提交点开第二次不再发请求。
*/
import type { PrCommit, PrCommitFile } from '../lib/messages'
import { useCallback, useEffect, useRef, useState } from 'react'
import { onMessage, postMessage } from '../lib/vscode'
export interface UsePrCommitsResult {
commits: PrCommit[]
commitsError: string | undefined
loadingCommits: boolean
filesBySha: Record<string, PrCommitFile[]>
parentShaBySha: Record<string, string | undefined>
filesErrorBySha: Record<string, string | undefined>
confirmedBySha: Record<string, string[]>
confirmedCommits: string[]
getFiles: (sha: string) => void
openDiff: (sha: string, parentSha: string | undefined, path: string, status: string) => void
setConfirmed: (sha: string, paths: string[]) => void
markCommitsConfirmed: (shas: string[]) => void
}
export function usePrCommits(issueNumber: number | undefined): UsePrCommitsResult {
const [commits, setCommits] = useState<PrCommit[]>([])
const [commitsError, setCommitsError] = useState<string | undefined>(undefined)
const [loadingCommits, setLoadingCommits] = useState(false)
const [filesBySha, setFilesBySha] = useState<Record<string, PrCommitFile[]>>({})
const [parentShaBySha, setParentShaBySha] = useState<Record<string, string | undefined>>({})
const [filesErrorBySha, setFilesErrorBySha] = useState<Record<string, string | undefined>>({})
const [confirmedBySha, setConfirmedBySha] = useState<Record<string, string[]>>({})
const [confirmedCommits, setConfirmedCommits] = useState<string[]>([])
// issueNumber 装进 ref,让订阅闭包始终读到当前值(订阅只在 mount 挂一次)。
const issueRef = useRef<number | undefined>(issueNumber)
issueRef.current = issueNumber
// 已发出 files 请求的 sha 集合:防同一提交并发重复请求(缓存到达前的窗口)。
const requestedRef = useRef<Set<string>>(new Set())
useEffect(() => {
const cleanup = onMessage((msg) => {
if (msg.type === 'pr-commits/show') {
if (msg.issueNumber !== issueRef.current)
return
setCommits(msg.commits)
setCommitsError(msg.error)
setConfirmedCommits(msg.confirmedCommits ?? [])
setLoadingCommits(false)
return
}
if (msg.type === 'pr-commit-files/show') {
if (msg.issueNumber !== issueRef.current)
return
setFilesBySha(prev => ({ ...prev, [msg.sha]: msg.files }))
setParentShaBySha(prev => ({ ...prev, [msg.sha]: msg.parentSha }))
setFilesErrorBySha(prev => ({ ...prev, [msg.sha]: msg.error }))
setConfirmedBySha(prev => ({ ...prev, [msg.sha]: msg.confirmed ?? [] }))
}
})
return cleanup
}, [])
// 切工单:清空缓存与已请求集合,重新拉提交列表。无工单时只清空。
useEffect(() => {
setCommits([])
setCommitsError(undefined)
setFilesBySha({})
setParentShaBySha({})
setFilesErrorBySha({})
setConfirmedBySha({})
setConfirmedCommits([])
requestedRef.current = new Set()
if (issueNumber === undefined) {
setLoadingCommits(false)
return
}
setLoadingCommits(true)
postMessage({ type: 'pr-commits/get', issueNumber })
}, [issueNumber])
const getFiles = useCallback((sha: string): void => {
const num = issueRef.current
if (num === undefined)
return
if (requestedRef.current.has(sha))
return
requestedRef.current.add(sha)
postMessage({ type: 'pr-commit-files/get', issueNumber: num, sha })
}, [])
const openDiff = useCallback(
(sha: string, parentSha: string | undefined, path: string, status: string): void => {
const num = issueRef.current
if (num === undefined)
return
postMessage({ type: 'pr-commit-diff/open', issueNumber: num, sha, parentSha, path, status })
},
[],
)
// 乐观更新本地确认态并落盘到扩展侧;下次拉文件回包会以持久化值校正。
const setConfirmed = useCallback((sha: string, paths: string[]): void => {
const num = issueRef.current
if (num === undefined)
return
setConfirmedBySha(prev => ({ ...prev, [sha]: paths }))
postMessage({ type: 'pr-review/set', issueNumber: num, sha, confirmed: paths })
}, [])
// 乐观更新本地已确认提交集并落盘;下次拉提交列表回包会以持久化值校正。
const markCommitsConfirmed = useCallback((shas: string[]): void => {
const num = issueRef.current
if (num === undefined)
return
setConfirmedCommits(shas)
postMessage({ type: 'pr-review/set-commits', issueNumber: num, confirmed: shas })
}, [])
return {
commits,
commitsError,
loadingCommits,
filesBySha,
parentShaBySha,
filesErrorBySha,
confirmedBySha,
confirmedCommits,
getFiles,
openDiff,
setConfirmed,
markCommitsConfirmed,
}
}
+130
View File
@@ -0,0 +1,130 @@
/**
* 「改动」tab 的订阅 hook:按工单号拉取整个 PR 的改动文件清单,并按需取单个文件
* 改动前后的原文(供 git-diff-view 内联渲染)。数据全程实时从扩展侧(Gitea)取,
* 本 hook 只做缓存与订阅。
*
* - issueNumber 变化(含首次)时 postMessage `pr-files/get`,并清空上一工单缓存。
* - 订阅 `pr-files/show` / `pr-file-diff/show`,只认 issueNumber 匹配的消息
* (扩展侧可能并发多工单的回包)。
* - diffByPath 缓存:同一文件点开第二次不再发请求。
* - 已确认(已查看)按 head sha 落盘,故 setConfirmed 要带当前 headSha。
*/
import type { PrFile } from '../lib/messages'
import { useCallback, useEffect, useRef, useState } from 'react'
import { onMessage, postMessage } from '../lib/vscode'
/** 单个文件改动前后的原文 + 语言推断;error 表示该文件取内容失败。 */
export interface FileDiff {
oldContent: string
newContent: string
oldLang?: string
newLang?: string
error?: string
}
export interface UsePrFilesResult {
files: PrFile[]
headSha: string
filesError: string | undefined
loading: boolean
confirmed: string[]
/** key ∈ diffByPath 即「已请求/已到」;值为内容。仅 key 在但值 undefined 不会出现。 */
diffByPath: Record<string, FileDiff>
getFileDiff: (path: string, previousPath?: string) => void
setConfirmed: (paths: string[]) => void
}
export function usePrFiles(issueNumber: number | undefined): UsePrFilesResult {
const [files, setFiles] = useState<PrFile[]>([])
const [headSha, setHeadSha] = useState<string>('')
const [filesError, setFilesError] = useState<string | undefined>(undefined)
const [loading, setLoading] = useState(false)
const [confirmed, setConfirmedState] = useState<string[]>([])
const [diffByPath, setDiffByPath] = useState<Record<string, FileDiff>>({})
// issueNumber / headSha 装进 ref,让订阅闭包与回调始终读到当前值。
const issueRef = useRef<number | undefined>(issueNumber)
issueRef.current = issueNumber
const headShaRef = useRef<string>('')
headShaRef.current = headSha
// 已发出 diff 请求的 path 集合:防同一文件并发重复请求(缓存到达前的窗口)。
const requestedRef = useRef<Set<string>>(new Set())
useEffect(() => {
const cleanup = onMessage((msg) => {
if (msg.type === 'pr-files/show') {
if (msg.issueNumber !== issueRef.current)
return
setFiles(msg.files)
setHeadSha(msg.headSha)
setConfirmedState(msg.confirmed ?? [])
setFilesError(msg.error)
setLoading(false)
return
}
if (msg.type === 'pr-file-diff/show') {
if (msg.issueNumber !== issueRef.current)
return
setDiffByPath(prev => ({
...prev,
[msg.path]: {
oldContent: msg.oldContent,
newContent: msg.newContent,
oldLang: msg.oldLang,
newLang: msg.newLang,
error: msg.error,
},
}))
}
})
return cleanup
}, [])
// 切工单:清空缓存与已请求集合,重新拉文件清单。无工单时只清空。
useEffect(() => {
setFiles([])
setHeadSha('')
setFilesError(undefined)
setConfirmedState([])
setDiffByPath({})
requestedRef.current = new Set()
if (issueNumber === undefined) {
setLoading(false)
return
}
setLoading(true)
postMessage({ type: 'pr-files/get', issueNumber })
}, [issueNumber])
const getFileDiff = useCallback((path: string, previousPath?: string): void => {
const num = issueRef.current
if (num === undefined)
return
if (requestedRef.current.has(path))
return
requestedRef.current.add(path)
postMessage({ type: 'pr-file-diff/get', issueNumber: num, path, previousPath })
}, [])
// 乐观更新本地确认态并落盘(键含当前 headSha);下次拉文件回包会以持久化值校正。
const setConfirmed = useCallback((paths: string[]): void => {
const num = issueRef.current
if (num === undefined)
return
setConfirmedState(paths)
postMessage({ type: 'pr-review/set', issueNumber: num, headSha: headShaRef.current, confirmed: paths })
}, [])
return {
files,
headSha,
filesError,
loading,
confirmed,
diffByPath,
getFileDiff,
setConfirmed,
}
}