✨ feat(vscode): 代码阅读预览按侧栏筛选算代码差异(未提交的更改对比上一次提交、审查对比 PR 目标分支;兄弟块按名字 / 相似度对齐,卡片内按行 / 值 / 词比对),算好后补发给预览
This commit is contained in:
1 parent
3db44f84ae
commit
2a5806c6b5
23 files changed
+1527
-31
No files matched your search
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { CHANGE_STYLES, changeOf } from './changedFiles'
|
||||
import { CHANGE_STYLES, changeOf, reviewBaseFile } from './changedFiles'
|
||||
|
||||
describe('改动状态', () => {
|
||||
it('新增 A、删除 D,别的(改名、复制、改了、没改)都算修改 M', () => {
|
||||
@@ -16,3 +16,18 @@ describe('改动状态', () => {
|
||||
expect(CHANGE_STYLES.D.tooltip).toBe('PR 里删掉了')
|
||||
})
|
||||
})
|
||||
|
||||
describe('审查时在 PR 目标分支上取哪个文件', () => {
|
||||
const files = [
|
||||
{ path: 'src/new.ts', status: 'added' },
|
||||
{ path: 'src/moved.ts', status: 'renamed', previousFilename: 'src/old.ts' },
|
||||
{ path: 'src/changed.ts', status: 'changed' },
|
||||
]
|
||||
|
||||
it('新加的目标分支上没有,改名的取改名前的路径,别的照原路径', () => {
|
||||
expect(reviewBaseFile(files, 'src/new.ts')).toEqual({ path: 'src/new.ts', added: true })
|
||||
expect(reviewBaseFile(files, 'src/moved.ts')).toEqual({ path: 'src/old.ts', added: false })
|
||||
expect(reviewBaseFile(files, 'src/changed.ts')).toEqual({ path: 'src/changed.ts', added: false })
|
||||
expect(reviewBaseFile(files, 'src/untouched.ts')).toEqual({ path: 'src/untouched.ts', added: false })
|
||||
})
|
||||
})
|
||||
@@ -8,6 +8,15 @@ export function changeOf(status: string): FileChange {
|
||||
return status === 'added' ? 'A' : status === 'deleted' ? 'D' : 'M'
|
||||
}
|
||||
|
||||
/**
|
||||
* 审查时工作副本里的一个文件(path 相对工作副本的根)在 PR 目标分支上叫什么、有没有:
|
||||
* 改名的取改名前的路径,PR 里新加的(Gitea 的 added)目标分支上没有。PR 没改过的文件照原路径取。
|
||||
*/
|
||||
export function reviewBaseFile(files: readonly { path: string, status: string, 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' }
|
||||
}
|
||||
|
||||
/** 审查时各种改动在侧栏上的样子:颜色用 VS Code 源代码管理的 git 装饰色(主题色 id),徽标就是字母本身。 */
|
||||
export const CHANGE_STYLES: Record<FileChange, { color: string, tooltip: string }> = {
|
||||
M: { color: 'gitDecoration.modifiedResourceForeground', tooltip: 'PR 里改了' },
|
||||
|
||||
@@ -2,17 +2,20 @@ import type { Disposable, ExtensionContext, QuickPickItem, TreeView, WorkspaceFo
|
||||
import type { PrFileDto } from '../../gitea/api'
|
||||
import type { RepoContext } from '../../gitea/repoContext'
|
||||
import type { Issue } from '../../gitea/types'
|
||||
import type { DiffBase } from '../diff/diffBase'
|
||||
import type { FileTreeProvider, TreeNode } from '../explorer/fileTree'
|
||||
import type { TreeFilter, TreeFilters } from '../explorer/treeFilters'
|
||||
import type { FileCount } from './pickList'
|
||||
import * as path from 'node:path'
|
||||
import { QuickPickItemKind, Uri, window, workspace } from 'vscode'
|
||||
import { fileAtRef } from '../../git/fileAt'
|
||||
import { logger } from '../../logging/logger'
|
||||
import { buildChangedTree } from '../explorer/changedTree'
|
||||
import { toPathKey } from '../pathMap'
|
||||
import { ChangeDecorations } from './changeDecorations'
|
||||
import { changeOf } from './changedFiles'
|
||||
import { changeOf, reviewBaseFile } from './changedFiles'
|
||||
import { countingFiles, pickEntries, pickItemText, pickRows, withFileCount } from './pickList'
|
||||
import { columnLabel, REVIEW_COLUMNS_TEXT, reviewableIssue, reviewablePullRequest, reviewDescription, reviewMessage } from './reviewRules'
|
||||
import { columnLabel, REVIEW_COLUMNS_TEXT, reviewableIssue, reviewablePullRequest, reviewBaseLabel, reviewBaseMissing, reviewDescription, reviewMessage } from './reviewRules'
|
||||
import { loadIssue, loadPullRequest, loadPullRequestFiles, requireRepo, reviewCandidateIssues } from './reviewSource'
|
||||
import { localWorktree, prepareWorktree } from './reviewWorktree'
|
||||
|
||||
@@ -25,12 +28,16 @@ const REVIEW_ISSUE_KEY = 'superpowers.codeReader.reviewIssue'
|
||||
*/
|
||||
type How = 'pick' | 'reload' | 'restore'
|
||||
|
||||
/** 进审查要的东西都备好了:工单、它的 PR、PR 改过的文件、装着 PR 最新代码的工作副本。 */
|
||||
/**
|
||||
* 进审查要的东西都备好了:工单、它的 PR、PR 改过的文件、装着 PR 最新代码的工作副本,
|
||||
* 和预览对比的基准:PR 的目标分支名、目标分支与 PR 头的合并基(Gitea 没给时退回目标分支的提交,同看板「改动」tab)。
|
||||
*/
|
||||
interface Loaded {
|
||||
issue: Issue
|
||||
pr: number
|
||||
files: PrFileDto[]
|
||||
worktree: string
|
||||
base: { ref: string, commit: string }
|
||||
}
|
||||
|
||||
/** 弹框里的一行:工单的一项带着工单,分组标题(列名)没有。 */
|
||||
@@ -44,14 +51,14 @@ function errorText(err: unknown): string {
|
||||
|
||||
/**
|
||||
* 侧栏按审查工单筛文件:「选工单」列出看板「审查」「进行中」列里能审查的工单,选中后树里只剩这张工单的 PR 改过的文件(按改动着色),
|
||||
* 点开的是本机这张工单的工作副本里 PR 最新的代码;「显示全部」回到工作区文件树。选的工单按工作区记住。
|
||||
* 点开的是本机这张工单的工作副本里 PR 最新的代码,预览对比 PR 的目标分支(diffBase);「显示全部」回到工作区文件树。选的工单按工作区记住。
|
||||
* 和别的筛选(未提交的更改)互斥,见 TreeFilters。
|
||||
*/
|
||||
export class ReviewMode implements TreeFilter, Disposable {
|
||||
private readonly decorations = new ChangeDecorations()
|
||||
private readonly disposables: Disposable[]
|
||||
/** 正在审查的工单号 */
|
||||
private reviewing: number | undefined
|
||||
/** 正在审查的:工单、PR、改过的文件、工作副本、对比基准 */
|
||||
private loaded: Loaded | undefined
|
||||
/** 每开始一次进审查、退出审查都加一:慢的那次回来时已经不是最新的,结果丢掉。 */
|
||||
private generation = 0
|
||||
|
||||
@@ -149,7 +156,7 @@ export class ReviewMode implements TreeFilter, Disposable {
|
||||
/** 「显示全部」或换成别的筛选:不再审查、不再记着这张工单,正在进的审查作废;正审查着的回到工作区文件树。 */
|
||||
async exit(): Promise<void> {
|
||||
this.generation++
|
||||
this.reviewing = undefined
|
||||
this.loaded = undefined
|
||||
this.decorations.show([])
|
||||
await Promise.all([
|
||||
this.filters.leave(this),
|
||||
@@ -159,8 +166,27 @@ export class ReviewMode implements TreeFilter, Disposable {
|
||||
|
||||
/** 审查时的「刷新」:重读工单和 PR 改过的文件,工作副本也更新到 PR 最新的提交。 */
|
||||
async reload(): Promise<void> {
|
||||
if (this.reviewing !== undefined)
|
||||
await this.review(this.reviewing, 'reload')
|
||||
if (this.loaded)
|
||||
await this.review(this.loaded.issue.number, 'reload')
|
||||
}
|
||||
|
||||
/**
|
||||
* 审查时,工作副本里的文件对比 PR 目标分支与 PR 头的合并基上的版本(改名的取改名前的路径);工作副本外面的文件不比。
|
||||
* 合并基是 PR 头的祖先,拉 PR 的提交时已经在本机了,直接在工作副本里取。
|
||||
*/
|
||||
diffBase(uri: Uri): DiffBase | undefined {
|
||||
const loaded = this.loaded
|
||||
const relative = loaded && toPathKey(Uri.file(loaded.worktree).path, uri.path)
|
||||
if (!loaded || relative === undefined || !loaded.base.commit)
|
||||
return undefined
|
||||
const { worktree, base } = loaded
|
||||
const file = reviewBaseFile(loaded.files, relative)
|
||||
return {
|
||||
label: reviewBaseLabel(base.ref),
|
||||
key: `${base.commit}:${file.path}`,
|
||||
read: () => file.added ? Promise.resolve(null) : fileAtRef(worktree, base.commit, file.path),
|
||||
missing: reviewBaseMissing(base.ref),
|
||||
}
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
@@ -240,17 +266,20 @@ export class ReviewMode implements TreeFilter, Disposable {
|
||||
if ('blocked' in reviewablePull)
|
||||
return reviewablePull
|
||||
const files = await loadPullRequestFiles(repo, pr)
|
||||
const worktree = await prepareWorktree(this.context, repo, issue, { number: pr, headSha: reviewablePull.pull.headSha }, step)
|
||||
return { issue, pr, files, worktree }
|
||||
const { headSha, baseRef, mergeBase, baseSha } = 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 } }
|
||||
}
|
||||
|
||||
private async show({ issue, pr, files, worktree }: Loaded): Promise<void> {
|
||||
private async show(loaded: Loaded): Promise<void> {
|
||||
const { issue, pr, files, worktree } = loaded
|
||||
const root = Uri.file(worktree)
|
||||
// 工作副本当根,名字照写工作区文件夹的:看的还是这个仓库,只是 PR 最新的代码
|
||||
const folder: WorkspaceFolder = { uri: root, name: workspace.workspaceFolders?.[0]?.name ?? path.basename(worktree), index: 0 }
|
||||
const changes = files.map(file => ({ path: file.path, change: changeOf(file.status) }))
|
||||
// 先记下再进:进的时候打开着的预览就来问对比基准
|
||||
this.loaded = loaded
|
||||
const entering = this.filters.enter(this, 'review')
|
||||
this.reviewing = issue.number
|
||||
this.files.showChanges([{ roots: [{ folder, tree: buildChangedTree(changes) }] }])
|
||||
this.decorations.show(changes.map(file => ({ uri: Uri.joinPath(root, file.path), change: file.change })))
|
||||
this.view.description = reviewDescription(issue.number)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { columnLabel, prNumberOf, REVIEW_COLUMNS_TEXT, reviewableIssue, reviewablePullRequest, reviewCandidates, reviewDescription, reviewMessage } from './reviewRules'
|
||||
import { columnLabel, prNumberOf, REVIEW_COLUMNS_TEXT, reviewableIssue, reviewablePullRequest, reviewBaseLabel, reviewBaseMissing, reviewCandidates, reviewDescription, reviewMessage } from './reviewRules'
|
||||
|
||||
const issue = { number: 446, title: '用户资料:上传头像,收紧非管理员改资料的权限' }
|
||||
|
||||
@@ -83,3 +83,12 @@ describe('还能不能审查(选中、刷新、重开窗口时判断)', () =
|
||||
expect(reviewablePullRequest(447, { state: 'closed', merged: false })).toEqual({ blocked: 'PR #447 已经关闭' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('审查时预览对比的基准', () => {
|
||||
it('对比 PR 要合进去的目标分支;目标分支上没有的文件说一句', () => {
|
||||
expect(reviewBaseLabel('master')).toBe('PR 目标分支 master')
|
||||
expect(reviewBaseMissing('master')).toBe('PR 目标分支 master 里没有这个文件')
|
||||
expect(reviewBaseLabel('')).toBe('PR 目标分支')
|
||||
expect(reviewBaseMissing('')).toBe('PR 目标分支里没有这个文件')
|
||||
})
|
||||
})
|
||||
@@ -76,3 +76,13 @@ export function reviewDescription(issueNumber: number): string {
|
||||
export function reviewMessage(issue: Pick<Issue, 'number' | 'title'>, pr: number, fileCount: number): string {
|
||||
return `#${issue.number} ${issue.title} · PR #${pr} · 改了 ${fileCount} 个文件`
|
||||
}
|
||||
|
||||
/** 审查时预览对比的基准(筛选栏上「对比:」后面的字):PR 要合进去的目标分支。 */
|
||||
export function reviewBaseLabel(baseRef: string): string {
|
||||
return baseRef ? `PR 目标分支 ${baseRef}` : 'PR 目标分支'
|
||||
}
|
||||
|
||||
/** PR 目标分支上没有这个文件(PR 里新加的)时横幅上的说明。 */
|
||||
export function reviewBaseMissing(baseRef: string): string {
|
||||
return baseRef ? `PR 目标分支 ${baseRef} 里没有这个文件` : 'PR 目标分支里没有这个文件'
|
||||
}
|
||||
Reference in new issue
Block a user