✨ feat(vscode): 代码阅读侧栏可以按审查工单只看 PR 改过的文件:标题栏「选工单」列出看板「审查」列,选中后树里只剩改过的文件(M / A / D 着色),点开的是本机工作副本里 PR 最新的代码,没有就拉 PR 分支建一个,「显示全部」退出
This commit is contained in:
1 parent
c009187f6c
commit
8e2cdfaf17
18 files changed
+1053
-40
No files matched your search
@@ -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)
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user