✨ 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:
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user