mirror of
https://github.com/stablyai/orca.git
synced 2026-09-30 08:03:12 +00:00
177 lines
7.0 KiB
TypeScript
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)
|
|
})
|
|
})
|