156 lines
6.4 KiB
TypeScript
156 lines
6.4 KiB
TypeScript
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)
|
||
})
|
||
})
|