mirror of
https://github.com/stablyai/orca.git
synced 2026-09-23 00:02:29 +00:00
fix: prune git worktree tracking after orphaned worktree removal (#428)
When removing an orphaned worktree, `git worktree remove` fails but the directory is cleaned up via `rm`. Without a subsequent `git worktree prune`, git's internal tracking remains stale — `git worktree list` continues to show the entry and the branch stays locked. Add `git worktree prune` after the rm in both the IPC handler and OrcaRuntimeService to fix this.
This commit is contained in:
@@ -18,7 +18,8 @@ const {
|
||||
hasHooksFileMock,
|
||||
loadHooksMock,
|
||||
computeWorktreePathMock,
|
||||
ensurePathWithinWorkspaceMock
|
||||
ensurePathWithinWorkspaceMock,
|
||||
gitExecFileAsyncMock
|
||||
} = vi.hoisted(() => ({
|
||||
handleMock: vi.fn(),
|
||||
removeHandlerMock: vi.fn(),
|
||||
@@ -36,7 +37,8 @@ const {
|
||||
hasHooksFileMock: vi.fn(),
|
||||
loadHooksMock: vi.fn(),
|
||||
computeWorktreePathMock: vi.fn(),
|
||||
ensurePathWithinWorkspaceMock: vi.fn()
|
||||
ensurePathWithinWorkspaceMock: vi.fn(),
|
||||
gitExecFileAsyncMock: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('electron', () => ({
|
||||
@@ -52,6 +54,11 @@ vi.mock('../git/worktree', () => ({
|
||||
removeWorktree: removeWorktreeMock
|
||||
}))
|
||||
|
||||
vi.mock('../git/runner', () => ({
|
||||
gitExecFileAsync: gitExecFileAsyncMock,
|
||||
gitExecFileSync: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('../git/repo', () => ({
|
||||
getGitUsername: getGitUsernameMock,
|
||||
getDefaultBaseRef: getDefaultBaseRefMock,
|
||||
@@ -120,6 +127,7 @@ describe('registerWorktreeHandlers', () => {
|
||||
loadHooksMock,
|
||||
computeWorktreePathMock,
|
||||
ensurePathWithinWorkspaceMock,
|
||||
gitExecFileAsyncMock,
|
||||
mainWindow.webContents.send,
|
||||
store.getRepos,
|
||||
store.getRepo,
|
||||
@@ -350,6 +358,28 @@ describe('registerWorktreeHandlers', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('prunes git worktree tracking when removing an orphaned worktree', async () => {
|
||||
const orphanError = Object.assign(new Error('git worktree remove failed'), {
|
||||
stderr: "fatal: '/workspace/feature-wt' is not a working tree"
|
||||
})
|
||||
removeWorktreeMock.mockRejectedValue(orphanError)
|
||||
getEffectiveHooksMock.mockReturnValue(null)
|
||||
gitExecFileAsyncMock.mockResolvedValue({ stdout: '', stderr: '' })
|
||||
|
||||
await handlers['worktrees:remove'](null, {
|
||||
worktreeId: 'repo-1::/workspace/feature-wt'
|
||||
})
|
||||
|
||||
// Should have called git worktree prune to clean up stale tracking
|
||||
expect(gitExecFileAsyncMock).toHaveBeenCalledWith(['worktree', 'prune'], {
|
||||
cwd: '/workspace/repo'
|
||||
})
|
||||
expect(store.removeWorktreeMeta).toHaveBeenCalledWith('repo-1::/workspace/feature-wt')
|
||||
expect(mainWindow.webContents.send).toHaveBeenCalledWith('worktrees:changed', {
|
||||
repoId: 'repo-1'
|
||||
})
|
||||
})
|
||||
|
||||
it('rejects ask-policy creates before mutating git state when setup decision is missing', async () => {
|
||||
getEffectiveHooksMock.mockReturnValue({
|
||||
scripts: {
|
||||
|
||||
@@ -12,7 +12,7 @@ import type {
|
||||
import { getPRForBranch } from '../github/client'
|
||||
import { listWorktrees, addWorktree, removeWorktree } from '../git/worktree'
|
||||
import { getGitUsername, getDefaultBaseRef, getBranchConflictKind } from '../git/repo'
|
||||
import { gitExecFileSync } from '../git/runner'
|
||||
import { gitExecFileAsync, gitExecFileSync } from '../git/runner'
|
||||
import { isWslPath, parseWslPath, getWslHome } from '../wsl'
|
||||
import { join } from 'path'
|
||||
import { listRepoWorktrees } from '../repo-worktrees'
|
||||
@@ -242,6 +242,11 @@ export function registerWorktreeHandlers(mainWindow: BrowserWindow, store: Store
|
||||
if (isOrphanedWorktreeError(error)) {
|
||||
console.warn(`[worktrees] Orphaned worktree detected at ${worktreePath}, cleaning up`)
|
||||
await rm(worktreePath, { recursive: true, force: true }).catch(() => {})
|
||||
// Why: `git worktree remove` failed, so git's internal worktree tracking
|
||||
// (`.git/worktrees/<name>`) is still intact. Without pruning, `git worktree
|
||||
// list` continues to show the stale entry and the branch it had checked out
|
||||
// remains locked — other worktrees cannot check it out.
|
||||
await gitExecFileAsync(['worktree', 'prune'], { cwd: repo.path }).catch(() => {})
|
||||
store.removeWorktreeMeta(args.worktreeId)
|
||||
await rebuildAuthorizedRootsCache(store)
|
||||
notifyWorktreesChanged(mainWindow, repoId)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* eslint-disable max-lines -- Why: the Orca runtime is the authoritative live control plane for the CLI, so handle validation, selector resolution, wait state, and summaries are kept together to avoid split-brain behavior. */
|
||||
/* eslint-disable unicorn/no-useless-spread -- Why: waiter sets and handle keys are cloned intentionally before mutation so resolution and rejection can safely remove entries while iterating. */
|
||||
/* eslint-disable no-control-regex -- Why: terminal normalization must strip ANSI and OSC control sequences from PTY output before returning bounded text to agents. */
|
||||
import { gitExecFileSync } from '../git/runner'
|
||||
import { gitExecFileAsync, gitExecFileSync } from '../git/runner'
|
||||
import { isWslPath, parseWslPath, getWslHome } from '../wsl'
|
||||
import { randomUUID } from 'crypto'
|
||||
import { join } from 'path'
|
||||
@@ -738,6 +738,11 @@ export class OrcaRuntimeService {
|
||||
} catch (error) {
|
||||
if (isOrphanedWorktreeError(error)) {
|
||||
await rm(worktree.path, { recursive: true, force: true }).catch(() => {})
|
||||
// Why: `git worktree remove` failed, so git's internal worktree tracking
|
||||
// (`.git/worktrees/<name>`) is still intact. Without pruning, `git worktree
|
||||
// list` continues to show the stale entry and the branch it had checked out
|
||||
// remains locked — other worktrees cannot check it out.
|
||||
await gitExecFileAsync(['worktree', 'prune'], { cwd: repo.path }).catch(() => {})
|
||||
this.store.removeWorktreeMeta(worktree.id)
|
||||
this.invalidateResolvedWorktreeCache()
|
||||
this.notifier?.worktreesChanged(repo.id)
|
||||
|
||||
Reference in New Issue
Block a user