♻️ 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,11 +1,12 @@
|
||||
import type { PrFileStatus } from '../../forge/types'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { CHANGE_STYLES, changeOf, reviewBaseFile, reviewTree } from './changedFiles'
|
||||
|
||||
describe('改动状态', () => {
|
||||
it('新增 A、删除 D,别的(改名、复制、改了、没改)都算修改 M', () => {
|
||||
it('新增 A、删除 D,别的(改名、复制、修改)都算修改 M', () => {
|
||||
expect(changeOf('added')).toBe('A')
|
||||
expect(changeOf('deleted')).toBe('D')
|
||||
for (const status of ['changed', 'modified', 'renamed', 'copied', 'unchanged', ''])
|
||||
for (const status of ['modified', 'renamed', 'copied'] as const)
|
||||
expect(changeOf(status)).toBe('M')
|
||||
})
|
||||
|
||||
@@ -18,10 +19,10 @@ describe('改动状态', () => {
|
||||
})
|
||||
|
||||
describe('审查时在 PR 目标分支上取哪个文件', () => {
|
||||
const files = [
|
||||
const files: { path: string, status: PrFileStatus, previousFilename?: string }[] = [
|
||||
{ path: 'src/new.ts', status: 'added' },
|
||||
{ path: 'src/moved.ts', status: 'renamed', previousFilename: 'src/old.ts' },
|
||||
{ path: 'src/changed.ts', status: 'changed' },
|
||||
{ path: 'src/changed.ts', status: 'modified' },
|
||||
]
|
||||
|
||||
it('新加的目标分支上没有,改名的取改名前的路径,别的照原路径', () => {
|
||||
@@ -33,13 +34,13 @@ describe('审查时在 PR 目标分支上取哪个文件', () => {
|
||||
})
|
||||
|
||||
describe('审查时树上摆哪些文件', () => {
|
||||
const files = [
|
||||
{ path: 'backend/app.py', status: 'changed' },
|
||||
const files: { path: string, status: PrFileStatus }[] = [
|
||||
{ path: 'backend/app.py', status: 'modified' },
|
||||
{ path: 'backend/test_app.py', status: 'added' },
|
||||
{ path: 'backend/tests/test_avatar.py', status: 'added' },
|
||||
{ path: 'backend/tests/fixtures/avatar.png', status: 'added' },
|
||||
{ path: 'web/src/__tests__/Avatar.tsx', status: 'changed' },
|
||||
{ path: 'web/src/Avatar.tsx', status: 'changed' },
|
||||
{ path: 'web/src/__tests__/Avatar.tsx', status: 'modified' },
|
||||
{ path: 'web/src/Avatar.tsx', status: 'modified' },
|
||||
]
|
||||
const names = (tree: ReturnType<typeof reviewTree>['tree'], dir: string): string[] => (tree.get(dir) ?? []).map(entry => entry.name)
|
||||
|
||||
@@ -62,7 +63,7 @@ describe('审查时树上摆哪些文件', () => {
|
||||
expect(names(tree, 'web/src')).toEqual(['__tests__', 'Avatar.tsx'])
|
||||
})
|
||||
|
||||
it('改动状态照 Gitea 的', () => {
|
||||
it('改动状态照 PR 文件的', () => {
|
||||
const { tree } = reviewTree(files, true)
|
||||
expect(tree.get('backend')?.[0]).toMatchObject({ path: 'backend/app.py', change: 'M' })
|
||||
})
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import type { PrFileStatus } from '../../forge/types'
|
||||
import type { ChangedTree, FileChange } from '../explorer/changedTree'
|
||||
import { buildChangedTree } from '../explorer/changedTree'
|
||||
import { isTestFile } from '../uncommitted/testFiles'
|
||||
|
||||
/**
|
||||
* Gitea 给的改动状态 → M / A / D。Gitea 的状态有 added、deleted、changed、renamed、copied、unchanged:
|
||||
* 新增、删除各算各的,别的都算修改(改名的在新路径上算修改)。
|
||||
*/
|
||||
export function changeOf(status: string): FileChange {
|
||||
/** PR 文件的改动类型 → M / A / D:新增、删除各算各的,别的都算修改(改名的在新路径上算修改)。 */
|
||||
export function changeOf(status: PrFileStatus): FileChange {
|
||||
return status === 'added' ? 'A' : status === 'deleted' ? 'D' : 'M'
|
||||
}
|
||||
|
||||
@@ -14,7 +12,7 @@ export function changeOf(status: string): FileChange {
|
||||
* 审查时树上摆的:PR 改过的文件和通往它们的目录。隐藏测试文件时先剔掉测试文件再摆,只装着测试文件的目录跟着不摆;
|
||||
* hidden 是剔了几个。
|
||||
*/
|
||||
export function reviewTree(files: readonly { path: string, status: string }[], hideTests: boolean): { tree: ChangedTree, hidden: number } {
|
||||
export function reviewTree(files: readonly { path: string, status: PrFileStatus }[], hideTests: boolean): { tree: ChangedTree, hidden: number } {
|
||||
const changes = files.map(file => ({ path: file.path, change: changeOf(file.status) }))
|
||||
const shown = hideTests ? changes.filter(file => !isTestFile(file.path)) : changes
|
||||
return { tree: buildChangedTree(shown), hidden: changes.length - shown.length }
|
||||
@@ -22,9 +20,9 @@ export function reviewTree(files: readonly { path: string, status: string }[], h
|
||||
|
||||
/**
|
||||
* 审查时工作副本里的一个文件(path 相对工作副本的根)在 PR 目标分支上叫什么、有没有:
|
||||
* 改名的取改名前的路径,PR 里新加的(Gitea 的 added)目标分支上没有。PR 没改过的文件照原路径取。
|
||||
* 改名的取改名前的路径,PR 里新加的目标分支上没有。PR 没改过的文件照原路径取。
|
||||
*/
|
||||
export function reviewBaseFile(files: readonly { path: string, status: string, previousFilename?: string }[], path: string): { path: string, added: boolean } {
|
||||
export function reviewBaseFile(files: readonly { path: string, status: PrFileStatus, previousFilename?: string }[], path: string): { path: string, added: boolean } {
|
||||
const file = files.find(item => item.path === path)
|
||||
return { path: file?.previousFilename ?? path, added: file?.status === 'added' }
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Issue } from '../../gitea/types'
|
||||
import type { Issue } from '../../issues/types'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { boardIssues } from '../../panel/kanbanSnapshot'
|
||||
import { countingFiles, pickEntries, pickItemText, pickRows, withFileCount } from './pickList'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Issue } from '../../gitea/types'
|
||||
import type { Issue } from '../../issues/types'
|
||||
import { columnLabel, prNumberOf } from './reviewRules'
|
||||
|
||||
/** PR 改了几个文件:还在读、没读到、读到了。 */
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import type { Disposable, Event, ExtensionContext, QuickPickItem, TreeView, WorkspaceFolder } from 'vscode'
|
||||
import type { PrFileDto } from '../../gitea/api'
|
||||
import type { RepoContext } from '../../gitea/repoContext'
|
||||
import type { Issue } from '../../gitea/types'
|
||||
import type { ForgeContext } from '../../forge/context'
|
||||
import type { ForgePrFile } from '../../forge/types'
|
||||
import type { Issue } from '../../issues/types'
|
||||
import type { DiffBase, DiffBaseSource } from '../diff/diffBase'
|
||||
import type { FileTreeProvider, TreeNode } from '../explorer/fileTree'
|
||||
import type { FileCount } from './pickList'
|
||||
import * as path from 'node:path'
|
||||
import { commands, EventEmitter, QuickPickItemKind, Uri, window, workspace } from 'vscode'
|
||||
import { requireForge } from '../../forge/context'
|
||||
import { fileAtRef } from '../../git/fileAt'
|
||||
import { logger } from '../../logging/logger'
|
||||
import { WORKSPACE_TREE } from '../explorer/fileTree'
|
||||
@@ -17,7 +18,7 @@ import { ChangeDecorations } from './changeDecorations'
|
||||
import { changeOf, reviewBaseFile, reviewTree } from './changedFiles'
|
||||
import { countingFiles, pickEntries, pickItemText, pickRows, withFileCount } from './pickList'
|
||||
import { columnLabel, REVIEW_COLUMNS_TEXT, reviewableIssue, reviewablePullRequest, reviewBaseLabel, reviewBaseMissing, reviewDescription, reviewMessage } from './reviewRules'
|
||||
import { loadIssue, loadPullRequest, loadPullRequestFiles, requireRepo, reviewCandidateIssues } from './reviewSource'
|
||||
import { loadIssue, loadPullRequest, reviewCandidateIssues } from './reviewSource'
|
||||
import { localWorktree, prepareWorktree } from './reviewWorktree'
|
||||
|
||||
/** workspaceState:正在审查的工单号,重开窗口接着审查。 */
|
||||
@@ -35,12 +36,12 @@ type How = 'pick' | 'reload' | 'restore'
|
||||
|
||||
/**
|
||||
* 进审查要的东西都备好了:工单、它的 PR、PR 改过的文件、装着 PR 最新代码的工作副本,
|
||||
* 和预览对比的基准:PR 的目标分支名、目标分支与 PR 头的合并基(Gitea 没给时退回目标分支的提交,同看板「改动」tab)。
|
||||
* 和预览对比的基准:PR 的目标分支名、目标分支与 PR 头的合并基(取不到时退回目标分支的提交,同看板「改动」tab)。
|
||||
*/
|
||||
interface Loaded {
|
||||
issue: Issue
|
||||
pr: number
|
||||
files: PrFileDto[]
|
||||
files: ForgePrFile[]
|
||||
worktree: string
|
||||
base: { ref: string, commit: string }
|
||||
}
|
||||
@@ -115,12 +116,12 @@ export class ReviewMode implements DiffBaseSource, Disposable {
|
||||
})
|
||||
picker.show()
|
||||
|
||||
let repo: RepoContext
|
||||
let repo: ForgeContext
|
||||
try {
|
||||
repo = await requireRepo(this.context)
|
||||
repo = await requireForge(this.context)
|
||||
}
|
||||
catch (err) {
|
||||
logger.add({ level: 'error', source: 'codeReader', message: '选工单:读不到当前工作区的 Gitea 仓库', details: errorText(err) })
|
||||
logger.add({ level: 'error', source: 'codeReader', message: '选工单:读不到当前工作区的代码托管仓库', details: errorText(err) })
|
||||
picker.hide()
|
||||
void window.showErrorMessage(`没法选工单:${errorText(err)}`)
|
||||
return
|
||||
@@ -228,9 +229,9 @@ export class ReviewMode implements DiffBaseSource, Disposable {
|
||||
|
||||
/** 弹框里的一项:有 PR 的工单去读 PR 改了几个文件(各项一起读),读不到就写读不到。 */
|
||||
/** 读一张工单的 PR 改了几个文件;没读到记进日志,这一项写读不到。 */
|
||||
private async countFiles(repo: RepoContext, issueNumber: number, pr: number): Promise<FileCount> {
|
||||
private async countFiles(repo: ForgeContext, issueNumber: number, pr: number): Promise<FileCount> {
|
||||
try {
|
||||
return { count: (await loadPullRequestFiles(repo, pr)).length }
|
||||
return { count: (await repo.provider.listPullRequestFiles(pr)).length }
|
||||
}
|
||||
catch (err) {
|
||||
logger.add({ level: 'warn', source: 'codeReader', message: `选工单:读不到 #${issueNumber} 的 PR #${pr} 改了几个文件`, details: errorText(err) })
|
||||
@@ -281,7 +282,7 @@ export class ReviewMode implements DiffBaseSource, Disposable {
|
||||
}
|
||||
|
||||
private async load(issueNumber: number, step: (text: string) => void): Promise<Loaded | { blocked: string }> {
|
||||
const repo = await requireRepo(this.context)
|
||||
const repo = await requireForge(this.context)
|
||||
step(`正在读取 #${issueNumber}…`)
|
||||
const reviewable = reviewableIssue(issueNumber, await loadIssue(this.context, repo, issueNumber))
|
||||
if ('blocked' in reviewable)
|
||||
@@ -291,10 +292,11 @@ export class ReviewMode implements DiffBaseSource, Disposable {
|
||||
const reviewablePull = reviewablePullRequest(pr, await loadPullRequest(repo, pr))
|
||||
if ('blocked' in reviewablePull)
|
||||
return reviewablePull
|
||||
const files = await loadPullRequestFiles(repo, pr)
|
||||
const { headSha, baseRef, mergeBase, baseSha } = reviewablePull.pull
|
||||
const files = await repo.provider.listPullRequestFiles(pr)
|
||||
const { headSha, baseRef } = reviewablePull.pull
|
||||
const baseCommit = await repo.provider.pullRequestDiffBase(reviewablePull.pull)
|
||||
const worktree = await prepareWorktree(this.context, repo, issue, { number: pr, headSha }, step)
|
||||
return { issue, pr, files, worktree, base: { ref: baseRef, commit: mergeBase || baseSha } }
|
||||
return { issue, pr, files, worktree, base: { ref: baseRef, commit: baseCommit } }
|
||||
}
|
||||
|
||||
private async show(loaded: Loaded): Promise<void> {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Issue, IssueColumn } from '../../gitea/types'
|
||||
import type { Issue, IssueColumn } from '../../issues/types'
|
||||
import { withHiddenTests } from '../uncommitted/testFiles'
|
||||
|
||||
/** 工单关联的 PR 号;state JSON 里没有、或不是正整数时没有。 */
|
||||
|
||||
@@ -1,28 +1,19 @@
|
||||
/**
|
||||
* 审查工单要的看板数据,全走看板自己的那套:列工单用看板上现在摆着的,重读一张工单用看板的单张加载
|
||||
* (state JSON 定列),本机记录(worktree 路径)照看板叠上,PR 用 Gitea 的接口。
|
||||
* (状态评论定列),本机记录(worktree 路径)照看板叠上,PR 用代码托管仓库的接口。
|
||||
*/
|
||||
|
||||
import type { ExtensionContext } from 'vscode'
|
||||
import type { GiteaPullRequest, PrFileDto } from '../../gitea/api'
|
||||
import type { RepoContext } from '../../gitea/repoContext'
|
||||
import type { Issue } from '../../gitea/types'
|
||||
import type { ForgeContext } from '../../forge/context'
|
||||
import type { ForgePullRequest } from '../../forge/types'
|
||||
import type { Issue } from '../../issues/types'
|
||||
import type { BoardIssues } from '../../panel/kanbanSnapshot'
|
||||
import { getPullRequest, GiteaApiError, listPullRequestFiles } from '../../gitea/api'
|
||||
import { loadSingleIssue } from '../../gitea/issueLoader'
|
||||
import { resolveRepoContext } from '../../gitea/repoContext'
|
||||
import { loadSingleIssue } from '../../forge/issueLoader'
|
||||
import { ForgeApiError } from '../../forge/types'
|
||||
import { overlayLocalIssueState } from '../../issues/localState'
|
||||
import { KanbanWebviewPanel } from '../../panel/KanbanPanel'
|
||||
import { reviewCandidates } from './reviewRules'
|
||||
|
||||
/** 当前工作区的 Gitea 仓库和 token;没有时抛出给人看的错。 */
|
||||
export async function requireRepo(context: ExtensionContext): Promise<RepoContext> {
|
||||
const repo = await resolveRepoContext(context)
|
||||
if (!repo)
|
||||
throw new Error('当前工作区没有 Gitea 仓库或未配置 token')
|
||||
return repo
|
||||
}
|
||||
|
||||
/**
|
||||
* 选工单弹框列的工单(哪些列、哪些工单见 reviewCandidates):取看板上现在摆着的(用户看到的就是它),不再加载一遍;
|
||||
* 看板还没加载过时 undefined。进审查时再单独重读选中的那张(loadIssue)。
|
||||
@@ -33,24 +24,19 @@ export function reviewCandidateIssues(context: ExtensionContext): BoardIssues |
|
||||
}
|
||||
|
||||
/** 一张工单的最新状态;工单已经不存在时 null。 */
|
||||
export async function loadIssue(context: ExtensionContext, repo: RepoContext, issueNumber: number): Promise<Issue | null> {
|
||||
const issue = await loadSingleIssue({ ...repo, issueNumber })
|
||||
return issue && overlayLocalIssueState(context, [issue], repo.workspaceRoot)[0]
|
||||
export async function loadIssue(context: ExtensionContext, forge: ForgeContext, issueNumber: number): Promise<Issue | null> {
|
||||
const issue = await loadSingleIssue(forge, issueNumber)
|
||||
return issue && overlayLocalIssueState(context, [issue], forge.workspaceRoot)[0]
|
||||
}
|
||||
|
||||
/** PR 的现状;PR 已经不存在时 null。 */
|
||||
export async function loadPullRequest(repo: RepoContext, pr: number): Promise<GiteaPullRequest | null> {
|
||||
export async function loadPullRequest(forge: ForgeContext, pr: number): Promise<ForgePullRequest | null> {
|
||||
try {
|
||||
return await getPullRequest({ ...repo, index: pr })
|
||||
return await forge.provider.getPullRequest(pr)
|
||||
}
|
||||
catch (err) {
|
||||
if (err instanceof GiteaApiError && err.status === 404)
|
||||
if (err instanceof ForgeApiError && err.status === 404)
|
||||
return null
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
/** PR 改过的全部文件。 */
|
||||
export function loadPullRequestFiles(repo: RepoContext, pr: number): Promise<PrFileDto[]> {
|
||||
return listPullRequestFiles({ ...repo, index: pr })
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ExtensionContext } from 'vscode'
|
||||
import type { RepoContext } from '../../gitea/repoContext'
|
||||
import type { Issue } from '../../gitea/types'
|
||||
import type { ForgeContext } from '../../forge/context'
|
||||
import type { Issue } from '../../issues/types'
|
||||
import { existsSync } from 'node:fs'
|
||||
import { window } from 'vscode'
|
||||
import { addDetachedWorktree, fastForwardWorktree, fetchPullRequestHead, resolveWorktreePath } from '../../git/worktree'
|
||||
@@ -34,7 +34,7 @@ export function localWorktree(context: ExtensionContext, workspaceRoot: string,
|
||||
*/
|
||||
export async function prepareWorktree(
|
||||
context: ExtensionContext,
|
||||
repo: RepoContext,
|
||||
repo: ForgeContext,
|
||||
issue: Pick<Issue, 'number' | 'worktreePath'>,
|
||||
pull: { number: number, headSha: string },
|
||||
step: (text: string) => void,
|
||||
|
||||
Reference in new issue
Block a user