Files
superwork/vscode/test/git/reviewWorktree.test.ts
T

156 lines
6.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
})
})