✨ 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:
+55
-92
@@ -216,6 +216,12 @@ export interface GiteaPullRequest {
|
||||
* the underlying issue number (`Closes #N`) when a comment fires on a PR.
|
||||
*/
|
||||
body: string
|
||||
/** head 分支最新提交 sha:PR diff 的右侧 ref。 */
|
||||
headSha: string
|
||||
/** base 分支 sha:mergeBase 取不到时的左侧 ref 兜底。 */
|
||||
baseSha: string
|
||||
/** base 与 head 的合并基:PR diff 的左侧 ref(三点 diff,与 Gitea 网页口径一致)。 */
|
||||
mergeBase: string
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -242,6 +248,9 @@ export async function getPullRequest(opts: {
|
||||
merged_at?: unknown
|
||||
html_url?: unknown
|
||||
body?: unknown
|
||||
head?: { sha?: unknown }
|
||||
base?: { sha?: unknown }
|
||||
merge_base?: unknown
|
||||
}
|
||||
return {
|
||||
number: typeof data.number === 'number' ? data.number : Number(data.number),
|
||||
@@ -250,6 +259,9 @@ export async function getPullRequest(opts: {
|
||||
merged_at: typeof data.merged_at === 'string' ? data.merged_at : null,
|
||||
html_url: typeof data.html_url === 'string' ? data.html_url : '',
|
||||
body: typeof data.body === 'string' ? data.body : '',
|
||||
headSha: typeof data.head?.sha === 'string' ? data.head.sha : '',
|
||||
baseSha: typeof data.base?.sha === 'string' ? data.base.sha : '',
|
||||
mergeBase: typeof data.merge_base === 'string' ? data.merge_base : '',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -513,113 +525,64 @@ export async function deleteWebhook(opts: {
|
||||
})
|
||||
}
|
||||
|
||||
/** PR 提交的精简视图:列表渲染只需 sha / 首行消息 / 作者 / 时间。 */
|
||||
export interface PrCommitDto {
|
||||
sha: string
|
||||
message: string
|
||||
authorName: string
|
||||
date: string
|
||||
}
|
||||
|
||||
/** 单个提交内的一个文件改动:path 给 diff 用,status 决定徽标颜色。 */
|
||||
export interface GitCommitFileDto {
|
||||
/** PR 改动里的一个文件:path/status 给文件树,additions/deletions 求和成统计条,previousFilename 用于改名时取左侧旧路径。 */
|
||||
export interface PrFileDto {
|
||||
path: string
|
||||
status: string
|
||||
}
|
||||
|
||||
/** 一个提交的详情:parentSha 用作 diff 左侧 ref,files 是改动清单。 */
|
||||
export interface GitCommitDetailDto {
|
||||
sha: string
|
||||
parentSha: string | undefined
|
||||
files: GitCommitFileDto[]
|
||||
additions: number
|
||||
deletions: number
|
||||
previousFilename?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 列出某 PR 的提交。关掉 files/stat/verification 让响应尽量小,列表只用到
|
||||
* 顶层字段。message 取首行(完整消息留给 commit 详情页,这里只做概览)。
|
||||
* 列出某 PR 的全部改动文件(三点 diff,merge_base → head,与 Gitea 网页 diff 口径一致)。
|
||||
* 分页拉到短页为止;改名文件带 previousFilename(旧路径,取左侧内容用)。
|
||||
*/
|
||||
export async function listPullRequestCommits(opts: {
|
||||
export async function listPullRequestFiles(opts: {
|
||||
host: string
|
||||
token: string
|
||||
owner: string
|
||||
repo: string
|
||||
index: number
|
||||
limit?: number
|
||||
}): Promise<PrCommitDto[]> {
|
||||
const url = new URL(
|
||||
`${baseUrl(opts.host)}/repos/${opts.owner}/${opts.repo}/pulls/${opts.index}/commits`,
|
||||
)
|
||||
url.searchParams.set('page', '1')
|
||||
url.searchParams.set('limit', String(opts.limit ?? 50))
|
||||
url.searchParams.set('files', 'false')
|
||||
url.searchParams.set('stat', 'false')
|
||||
url.searchParams.set('verification', 'false')
|
||||
}): Promise<PrFileDto[]> {
|
||||
const out: PrFileDto[] = []
|
||||
let page = 1
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
const url = new URL(
|
||||
`${baseUrl(opts.host)}/repos/${opts.owner}/${opts.repo}/pulls/${opts.index}/files`,
|
||||
)
|
||||
url.searchParams.set('limit', String(PAGE_SIZE))
|
||||
url.searchParams.set('page', String(page))
|
||||
|
||||
const res = await fetch(url.toString(), { headers: authHeaders(opts.token) })
|
||||
await ensureOk(res)
|
||||
const data = await res.json() as Array<{
|
||||
sha?: unknown
|
||||
created?: unknown
|
||||
commit?: { message?: unknown, author?: { name?: unknown, date?: unknown } }
|
||||
author?: { login?: unknown } | null
|
||||
}>
|
||||
if (!Array.isArray(data))
|
||||
return []
|
||||
const result = data.map((c) => {
|
||||
const fullMessage = typeof c.commit?.message === 'string' ? c.commit.message : ''
|
||||
const message = fullMessage.split('\n', 1)[0] ?? ''
|
||||
const authorName = typeof c.commit?.author?.name === 'string' ? c.commit.author.name : ''
|
||||
const commitDate = typeof c.commit?.author?.date === 'string' ? c.commit.author.date : ''
|
||||
const created = typeof c.created === 'string' ? c.created : ''
|
||||
return {
|
||||
sha: typeof c.sha === 'string' ? c.sha : '',
|
||||
message,
|
||||
authorName,
|
||||
date: commitDate || created,
|
||||
}
|
||||
})
|
||||
// 按提交时间升序(最早在最前)。ISO 字符串字典序即时间序;date 为空者排前面。
|
||||
result.sort((a, b) => a.date.localeCompare(b.date))
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* 取单个提交详情,带文件清单。parentSha(首个父提交)作为 diff 左侧 ref;
|
||||
* 根提交无 parent 时为 undefined,调用方据此把左侧当空内容处理(呈现全新增)。
|
||||
*/
|
||||
export async function getGitCommit(opts: {
|
||||
host: string
|
||||
token: string
|
||||
owner: string
|
||||
repo: string
|
||||
sha: string
|
||||
}): Promise<GitCommitDetailDto> {
|
||||
const url = new URL(
|
||||
`${baseUrl(opts.host)}/repos/${opts.owner}/${opts.repo}/git/commits/${opts.sha}`,
|
||||
)
|
||||
url.searchParams.set('files', 'true')
|
||||
url.searchParams.set('stat', 'false')
|
||||
url.searchParams.set('verification', 'false')
|
||||
|
||||
const res = await fetch(url.toString(), { headers: authHeaders(opts.token) })
|
||||
await ensureOk(res)
|
||||
const data = await res.json() as {
|
||||
sha?: unknown
|
||||
parents?: Array<{ sha?: unknown }>
|
||||
files?: Array<{ filename?: unknown, status?: unknown }>
|
||||
}
|
||||
const parentSha = typeof data.parents?.[0]?.sha === 'string' ? data.parents[0].sha : undefined
|
||||
const files = Array.isArray(data.files)
|
||||
? data.files.map(f => ({
|
||||
const res = await fetch(url.toString(), { headers: authHeaders(opts.token) })
|
||||
await ensureOk(res)
|
||||
const batch = await res.json() as Array<{
|
||||
filename?: unknown
|
||||
previous_filename?: unknown
|
||||
status?: unknown
|
||||
additions?: unknown
|
||||
deletions?: unknown
|
||||
}>
|
||||
if (!Array.isArray(batch) || batch.length === 0)
|
||||
break
|
||||
for (const f of batch) {
|
||||
const prev = typeof f.previous_filename === 'string' && f.previous_filename.length > 0
|
||||
? f.previous_filename
|
||||
: undefined
|
||||
out.push({
|
||||
path: typeof f.filename === 'string' ? f.filename : '',
|
||||
status: typeof f.status === 'string' ? f.status : '',
|
||||
}))
|
||||
: []
|
||||
return {
|
||||
sha: typeof data.sha === 'string' ? data.sha : opts.sha,
|
||||
parentSha,
|
||||
files,
|
||||
additions: typeof f.additions === 'number' ? f.additions : 0,
|
||||
deletions: typeof f.deletions === 'number' ? f.deletions : 0,
|
||||
previousFilename: prev,
|
||||
})
|
||||
}
|
||||
if (batch.length < PAGE_SIZE)
|
||||
break
|
||||
page += 1
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user