✨ feat(vscode): 代码阅读侧栏可以按审查工单只看 PR 改过的文件:标题栏「选工单」列出看板「审查」列,选中后树里只剩改过的文件(M / A / D 着色),点开的是本机工作副本里 PR 最新的代码,没有就拉 PR 分支建一个,「显示全部」退出

This commit is contained in:
cruldra committed 2026-09-27 17:36:02 +08:00
1 parent c009187f6c
commit 8e2cdfaf17
18 files changed
+1053 -40

No files matched your search

+155
View File
@@ -0,0 +1,155 @@
import { execFileSync } from 'node:child_process'
import { existsSync, promises as fsp, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs'
import * as os from 'node:os'
import * as path from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import { addDetachedWorktree, fastForwardWorktree, fetchPullRequestHead } from '../../src/git/worktree'
const gitEnv: NodeJS.ProcessEnv = {
...process.env,
GIT_CONFIG_NOSYSTEM: '1',
GIT_CONFIG_GLOBAL: '/dev/null',
GIT_TERMINAL_PROMPT: '0',
GIT_AUTHOR_NAME: 'review-test',
GIT_AUTHOR_EMAIL: 'review@test.local',
GIT_COMMITTER_NAME: 'review-test',
GIT_COMMITTER_EMAIL: 'review@test.local',
}
function git(cwd: string, args: string[]): string {
return execFileSync('git', ['-C', cwd, ...args], { encoding: 'utf8', env: gitEnv }).trim()
}
function commit(cwd: string, file: string, content: string): string {
writeFileSync(path.join(cwd, file), content)
git(cwd, ['add', file])
git(cwd, ['commit', '-m', `edit ${file}`])
return git(cwd, ['rev-parse', 'HEAD'])
}
interface Fixture { root: string, peer: string, repo: string, worktree: string }
/** origin(bare)上有 main,同事把 PR 的提交推到 Gitea 给 PR 留的 refs/pull/7/head;repo 是审查方的 clone,本机没有 PR 的分支。 */
async function setup(): Promise<Fixture> {
const root = realpathSync(await fsp.mkdtemp(path.join(os.tmpdir(), 'sw-review-')))
const origin = path.join(root, 'origin.git')
git(root, ['init', '--bare', '-b', 'main', origin])
const peer = path.join(root, 'peer')
git(root, ['clone', origin, peer])
commit(peer, 'README.md', 'hello\n')
git(peer, ['push', '-u', 'origin', 'main'])
git(peer, ['checkout', '-b', 'feature/x'])
commit(peer, 'x.txt', 'x1\n')
git(peer, ['push', 'origin', 'HEAD:refs/pull/7/head'])
const repo = path.join(root, 'repo')
git(root, ['clone', origin, repo])
return { root, peer, repo, worktree: path.join(root, 'wt', 'repo', 'review-6') }
}
/** 同事在 PR 上又推了一个提交 */
function pushToPullRequest(fx: Fixture, content: string): string {
const sha = commit(fx.peer, 'x.txt', content)
git(fx.peer, ['push', 'origin', 'HEAD:refs/pull/7/head'])
return sha
}
let fx: Fixture | undefined
afterEach(() => {
if (fx)
rmSync(fx.root, { recursive: true, force: true })
fx = undefined
})
async function reviewWorktree(fixture: Fixture): Promise<string> {
await fetchPullRequestHead(fixture.repo, 7)
const head = git(fixture.peer, ['rev-parse', 'HEAD'])
await addDetachedWorktree({ workspaceRoot: fixture.repo, worktreePath: fixture.worktree, commit: head })
return head
}
describe('审查用的 worktree', () => {
it('从 refs/pull/<n>/head 拉 PR 的提交,建一个停在它上面、不挂分支的 worktree(父目录不在也建)', async () => {
fx = await setup()
const head = await reviewWorktree(fx)
expect(readFileSync(path.join(fx.worktree, 'x.txt'), 'utf8')).toBe('x1\n')
expect(git(fx.worktree, ['rev-parse', 'HEAD'])).toBe(head)
expect(git(fx.worktree, ['rev-parse', '--abbrev-ref', 'HEAD'])).toBe('HEAD')
expect(git(fx.repo, ['branch', '--list'])).not.toContain('feature/x')
})
it('目录被手动删掉后同一路径能再建', async () => {
fx = await setup()
await reviewWorktree(fx)
rmSync(fx.worktree, { recursive: true, force: true })
await reviewWorktree(fx)
expect(existsSync(path.join(fx.worktree, 'x.txt'))).toBe(true)
})
})
describe('把 worktree 快进到 PR 最新的提交', () => {
it('同事在 PR 上又推了提交、工作副本干净:快进过去', async () => {
fx = await setup()
await reviewWorktree(fx)
const latest = pushToPullRequest(fx, 'x2\n')
await fetchPullRequestHead(fx.repo, 7)
expect(await fastForwardWorktree(fx.worktree, latest)).toBe('updated')
expect(git(fx.worktree, ['rev-parse', 'HEAD'])).toBe(latest)
expect(readFileSync(path.join(fx.worktree, 'x.txt'), 'utf8')).toBe('x2\n')
})
it('已经在最新的提交上:不动', async () => {
fx = await setup()
const head = await reviewWorktree(fx)
expect(await fastForwardWorktree(fx.worktree, head)).toBe('current')
})
it('只有未跟踪的文件:照样快进,文件留着', async () => {
fx = await setup()
await reviewWorktree(fx)
writeFileSync(path.join(fx.worktree, 'notes.txt'), 'mine\n')
const latest = pushToPullRequest(fx, 'x2\n')
await fetchPullRequestHead(fx.repo, 7)
expect(await fastForwardWorktree(fx.worktree, latest)).toBe('updated')
expect(readFileSync(path.join(fx.worktree, 'notes.txt'), 'utf8')).toBe('mine\n')
})
it('有没提交的改动:不动,改动留着', async () => {
fx = await setup()
const head = await reviewWorktree(fx)
writeFileSync(path.join(fx.worktree, 'x.txt'), 'local edit\n')
const latest = pushToPullRequest(fx, 'x2\n')
await fetchPullRequestHead(fx.repo, 7)
expect(await fastForwardWorktree(fx.worktree, latest)).toBe('dirty')
expect(git(fx.worktree, ['rev-parse', 'HEAD'])).toBe(head)
expect(readFileSync(path.join(fx.worktree, 'x.txt'), 'utf8')).toBe('local edit\n')
})
it('本地另有提交、PR 也往前走了:快进不了,不动', async () => {
fx = await setup()
await reviewWorktree(fx)
const local = commit(fx.worktree, 'local.txt', 'local\n')
const latest = pushToPullRequest(fx, 'x2\n')
await fetchPullRequestHead(fx.repo, 7)
expect(await fastForwardWorktree(fx.worktree, latest)).toBe('diverged')
expect(git(fx.worktree, ['rev-parse', 'HEAD'])).toBe(local)
})
it('目录不是工作副本的根(只是仓库里的普通目录):报错,外面的仓库不动', async () => {
fx = await setup()
await fetchPullRequestHead(fx.repo, 7)
const latest = git(fx.peer, ['rev-parse', 'HEAD'])
const before = git(fx.repo, ['rev-parse', 'HEAD'])
const inside = path.join(fx.repo, 'review-6')
await fsp.mkdir(inside)
await expect(fastForwardWorktree(inside, latest)).rejects.toThrow('不是 git 工作副本的根目录')
expect(git(fx.repo, ['rev-parse', 'HEAD'])).toBe(before)
})
it('本地在 PR 最新的提交之上还有提交:已经含着它,不动', async () => {
fx = await setup()
const head = await reviewWorktree(fx)
const local = commit(fx.worktree, 'local.txt', 'local\n')
expect(await fastForwardWorktree(fx.worktree, head)).toBe('current')
expect(git(fx.worktree, ['rev-parse', 'HEAD'])).toBe(local)
})
})