mirror of
https://github.com/stablyai/orca.git
synced 2026-09-29 08:03:20 +00:00
Preserve preparation ownership until Git removal starts
This commit is contained in:
@@ -55,6 +55,39 @@ afterEach(async () => {
|
||||
})
|
||||
|
||||
describe('prepared worktree creation with real Git', () => {
|
||||
it('retains preparation ownership when the removal command cannot start', async () => {
|
||||
const fixture = await createRepo()
|
||||
const repoPath = await realpath(fixture.repoPath)
|
||||
const root = await realpath(fixture.root)
|
||||
const preparedPath = join(root, WORKTREE_CREATE_PREPARATION_DIRECTORY, 'owned-removal')
|
||||
await mkdir(join(root, WORKTREE_CREATE_PREPARATION_DIRECTORY), { recursive: true })
|
||||
const lockReason = createWorktreePreparationLockReason('removal-failure')
|
||||
await prepareWorktreeCreateCheckout(repoPath, preparedPath, 'main', lockReason)
|
||||
const original = gitRunner.gitExecFileAsync
|
||||
const spy = vi.spyOn(gitRunner, 'gitExecFileAsync').mockImplementation((args, options) => {
|
||||
if (args.includes('remove') && args.includes(preparedPath)) {
|
||||
return Promise.reject(new Error('injected removal launch failure'))
|
||||
}
|
||||
return original(args, options)
|
||||
})
|
||||
try {
|
||||
await expect(discardPreparedWorktree(repoPath, preparedPath)).rejects.toThrow(
|
||||
'injected removal launch failure'
|
||||
)
|
||||
const remaining = await listWorktrees(repoPath, { includeCreatePreparations: true })
|
||||
const prepared = remaining.find((worktree) =>
|
||||
areWorktreePathsEqual(worktree.path, preparedPath)
|
||||
)
|
||||
expect(prepared).toBeDefined()
|
||||
expect(prepared?.lockReason).toBe(lockReason)
|
||||
expect(await readFile(join(preparedPath, 'version.txt'), 'utf8')).toBe('one\n')
|
||||
} finally {
|
||||
spy.mockRestore()
|
||||
await discardPreparedWorktree(repoPath, preparedPath)
|
||||
}
|
||||
expect(existsSync(preparedPath)).toBe(false)
|
||||
})
|
||||
|
||||
it('creates and finalizes while dead-owner reclamation is stalled', async () => {
|
||||
const fixture = await createRepo()
|
||||
const repoPath = await realpath(fixture.repoPath)
|
||||
|
||||
@@ -45,16 +45,16 @@ async function performDiscardPreparedWorktree(
|
||||
timeout: options.timeout ?? WORKTREE_REMOVAL_REGISTRATION_TIMEOUT_MS
|
||||
}
|
||||
try {
|
||||
// Preserve the ownership lock if removal cannot start; Git 2.25 supports locked removal.
|
||||
await gitExecFileAsync(
|
||||
[...windowsLongPathGitArgs(repoPath), 'worktree', 'unlock', worktreePath],
|
||||
cleanupGitOptions
|
||||
)
|
||||
} catch {
|
||||
// It may be unlocked already or only partially registered.
|
||||
}
|
||||
try {
|
||||
await gitExecFileAsync(
|
||||
[...windowsLongPathGitArgs(repoPath), 'worktree', 'remove', '--force', worktreePath],
|
||||
[
|
||||
...windowsLongPathGitArgs(repoPath),
|
||||
'worktree',
|
||||
'remove',
|
||||
'--force',
|
||||
'--force',
|
||||
worktreePath
|
||||
],
|
||||
cleanupGitOptions
|
||||
)
|
||||
} finally {
|
||||
|
||||
@@ -204,6 +204,16 @@ describeBinaryCompatibility('real Git binary compatibility', () => {
|
||||
await rm(join(repoPath, 'deferred-trash'), { recursive: true, force: true })
|
||||
})
|
||||
|
||||
it('removes locked prepared worktrees without a separate unlock', async () => {
|
||||
await runGit(['worktree', 'add', '--detach', '--no-checkout', 'compat-discard', 'HEAD'])
|
||||
await runGit(['-C', 'compat-discard', 'reset', '--hard', 'HEAD'])
|
||||
await runGit(['worktree', 'lock', '--reason', 'owned preparation', 'compat-discard'])
|
||||
await runGit(['worktree', 'remove', '--force', '--force', 'compat-discard'])
|
||||
expect((await runGit(['worktree', 'list', '--porcelain'])).stdout).not.toContain(
|
||||
'compat-discard'
|
||||
)
|
||||
})
|
||||
|
||||
it('supports prepared worktree creation and finalization', async () => {
|
||||
await runGit(['worktree', 'add', '--detach', '--no-checkout', 'compat-prepared', 'HEAD'])
|
||||
await runGit(['-C', 'compat-prepared', 'reset', '--hard', 'HEAD'])
|
||||
|
||||
Reference in New Issue
Block a user