Files
orca/src/main/git/worktree-create-preparation-real-git.test.ts
T

177 lines
7.0 KiB
TypeScript

import { execFileSync } from 'node:child_process'
import { mkdir, mkdtemp, readFile, realpath, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import {
createWorktreePreparationLockReason,
isWorktreeCreatePreparation,
WORKTREE_CREATE_PREPARATION_DIRECTORY
} from '../../shared/worktree/create-preparation'
import { listWorktrees } from './worktree'
import {
discardPreparedWorktree,
finalizePreparedWorktree,
prepareWorktreeCreateCheckout
} from './worktree-create-preparation'
import { areWorktreePathsEqual } from './worktree-path-comparison'
const tempRoots: string[] = []
function git(cwd: string, args: string[]): string {
return execFileSync('git', args, {
cwd,
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe']
}).trim()
}
async function createRepo(): Promise<{ repoPath: string; root: string }> {
const root = await mkdtemp(join(tmpdir(), 'orca-prepared-worktree-'))
tempRoots.push(root)
const repoPath = join(root, 'repo')
execFileSync('git', ['init', '--quiet', repoPath])
git(repoPath, ['symbolic-ref', 'HEAD', 'refs/heads/main'])
git(repoPath, ['config', 'user.email', 'test@example.com'])
git(repoPath, ['config', 'user.name', 'Test User'])
git(repoPath, ['config', 'core.autocrlf', 'false'])
await writeFile(join(repoPath, 'version.txt'), 'one\n')
git(repoPath, ['add', 'version.txt'])
git(repoPath, ['commit', '--quiet', '-m', 'initial'])
return { repoPath, root }
}
afterEach(async () => {
await Promise.all(tempRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })))
})
describe('prepared worktree creation with real Git', () => {
it('cleans up when the create signal is canceled', async () => {
const { repoPath, root } = await createRepo()
const preparationRoot = join(root, WORKTREE_CREATE_PREPARATION_DIRECTORY)
const preparedPath = join(preparationRoot, `${process.pid}-canceled`)
await mkdir(preparationRoot, { recursive: true })
await prepareWorktreeCreateCheckout(
repoPath,
preparedPath,
'main',
createWorktreePreparationLockReason('canceled-test')
)
const controller = new AbortController()
controller.abort()
await expect(
discardPreparedWorktree(repoPath, preparedPath, { signal: controller.signal })
).resolves.toBeUndefined()
expect(await listWorktrees(repoPath, { includeCreatePreparations: true })).toHaveLength(1)
})
it('lands a cross-base retarget on exactly the requested commit', async () => {
const { repoPath, root } = await createRepo()
const preparationRoot = join(root, WORKTREE_CREATE_PREPARATION_DIRECTORY)
const preparedPath = join(preparationRoot, `${process.pid}-retarget`)
const finalPath = join(root, 'retargeted-worktree')
await mkdir(preparationRoot, { recursive: true })
await writeFile(join(repoPath, 'shared.txt'), 'kept\n')
git(repoPath, ['add', 'shared.txt'])
git(repoPath, ['commit', '--quiet', '-m', 'local main'])
const localMainHead = git(repoPath, ['rev-parse', 'HEAD'])
// A remote-tracking `main` that diverged: different content, an extra file, and one deletion.
git(repoPath, ['checkout', '--quiet', '-b', 'upstream-main'])
await writeFile(join(repoPath, 'version.txt'), 'two\n')
await writeFile(join(repoPath, 'only-upstream.txt'), 'upstream\n')
git(repoPath, ['rm', '--quiet', 'shared.txt'])
git(repoPath, ['add', 'version.txt', 'only-upstream.txt'])
git(repoPath, ['commit', '--quiet', '-m', 'upstream main'])
git(repoPath, ['update-ref', 'refs/remotes/origin/main', 'HEAD'])
git(repoPath, ['checkout', '--quiet', 'main'])
git(repoPath, ['branch', '--quiet', '-D', 'upstream-main'])
await prepareWorktreeCreateCheckout(
repoPath,
preparedPath,
'refs/remotes/origin/main',
createWorktreePreparationLockReason('retarget-test')
)
expect(git(preparedPath, ['rev-parse', 'HEAD'])).not.toBe(localMainHead)
await finalizePreparedWorktree(repoPath, preparedPath, finalPath, 'feature/retargeted', 'main')
expect(git(finalPath, ['rev-parse', 'HEAD'])).toBe(localMainHead)
// A retarget that left stale files behind would be a wrong checkout, not just a slow one.
expect(git(finalPath, ['status', '--porcelain'])).toBe('')
expect((await readFile(join(finalPath, 'version.txt'), 'utf8')).replaceAll('\r\n', '\n')).toBe(
'one\n'
)
expect((await readFile(join(finalPath, 'shared.txt'), 'utf8')).replaceAll('\r\n', '\n')).toBe(
'kept\n'
)
await expect(readFile(join(finalPath, 'only-upstream.txt'), 'utf8')).rejects.toThrow()
expect(git(finalPath, ['branch', '--show-current'])).toBe('feature/retargeted')
expect(git(finalPath, ['config', '--get', 'branch.feature/retargeted.base'])).toBe(
'refs/heads/main'
)
})
it('hides the preparation, retargets an advanced base, and attaches the final branch', async () => {
const { repoPath, root } = await createRepo()
const preparationRoot = join(root, WORKTREE_CREATE_PREPARATION_DIRECTORY)
const preparedPath = join(preparationRoot, `${process.pid}-test`)
const finalPath = join(root, 'final-worktree')
await mkdir(preparationRoot, { recursive: true })
await prepareWorktreeCreateCheckout(
repoPath,
preparedPath,
'main',
createWorktreePreparationLockReason('real-git-test')
)
const visibleBeforeSubmit = await listWorktrees(repoPath)
const allBeforeSubmit = await listWorktrees(repoPath, { includeCreatePreparations: true })
expect(visibleBeforeSubmit).toHaveLength(1)
expect(allBeforeSubmit).toHaveLength(2)
expect(allBeforeSubmit.find(isWorktreeCreatePreparation)).toMatchObject({
locked: true,
lockReason: expect.stringContaining('orca-create-preparation:v1:')
})
await writeFile(join(repoPath, 'version.txt'), 'two\n')
git(repoPath, ['add', 'version.txt'])
git(repoPath, ['commit', '--quiet', '-m', 'advance base'])
const latestHead = git(repoPath, ['rev-parse', 'HEAD'])
await finalizePreparedWorktree(
repoPath,
preparedPath,
finalPath,
'feature/prepared',
'main',
false
)
expect(git(finalPath, ['rev-parse', 'HEAD'])).toBe(latestHead)
expect(git(finalPath, ['branch', '--show-current'])).toBe('feature/prepared')
expect((await readFile(join(finalPath, 'version.txt'), 'utf8')).replaceAll('\r\n', '\n')).toBe(
'two\n'
)
expect(git(finalPath, ['config', '--get', 'branch.feature/prepared.base'])).toBe(
'refs/heads/main'
)
expect(git(finalPath, ['config', '--get', 'push.autoSetupRemote'])).toBe('true')
const listedWorktrees = await listWorktrees(repoPath)
const resolvedFinalPath = await realpath(finalPath)
expect(
listedWorktrees.some((worktree) => areWorktreePathsEqual(worktree.path, resolvedFinalPath))
).toBe(true)
expect(
listedWorktrees.find((worktree) => areWorktreePathsEqual(worktree.path, resolvedFinalPath))
?.locked
).not.toBe(true)
})
})