mirror of
https://github.com/stablyai/orca
synced 2026-04-21 14:17:16 +00:00
fix: handle orphaned worktree deletion with disk cleanup (#109)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
69978c93f5
commit
902e2271b9
4 changed files with 53 additions and 2 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -47,6 +47,7 @@ coverage/
|
|||
tmp/
|
||||
.tmp/
|
||||
design-docs/
|
||||
.context/
|
||||
|
||||
# Stably CLI (only docs/ are tracked)
|
||||
.stably/*
|
||||
|
|
|
|||
|
|
@ -8,7 +8,8 @@ import {
|
|||
shouldSetDisplayName,
|
||||
mergeWorktree,
|
||||
parseWorktreeId,
|
||||
formatWorktreeRemovalError
|
||||
formatWorktreeRemovalError,
|
||||
isOrphanedWorktreeError
|
||||
} from './worktree-logic'
|
||||
|
||||
describe('sanitizeWorktreeName', () => {
|
||||
|
|
@ -254,3 +255,29 @@ describe('formatWorktreeRemovalError', () => {
|
|||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('isOrphanedWorktreeError', () => {
|
||||
it('returns true when stderr contains "is not a working tree"', () => {
|
||||
const error = Object.assign(new Error('git failed'), {
|
||||
stderr: "fatal: '/some/path' is not a working tree"
|
||||
})
|
||||
expect(isOrphanedWorktreeError(error)).toBe(true)
|
||||
})
|
||||
|
||||
it('returns true when message contains "is not a working tree"', () => {
|
||||
const error = new Error("fatal: '/some/path' is not a working tree")
|
||||
expect(isOrphanedWorktreeError(error)).toBe(true)
|
||||
})
|
||||
|
||||
it('returns false for unrelated git errors', () => {
|
||||
const error = Object.assign(new Error('git failed'), {
|
||||
stderr: 'fatal: contains modified or untracked files'
|
||||
})
|
||||
expect(isOrphanedWorktreeError(error)).toBe(false)
|
||||
})
|
||||
|
||||
it('returns false for non-Error input', () => {
|
||||
expect(isOrphanedWorktreeError('string error')).toBe(false)
|
||||
expect(isOrphanedWorktreeError(null)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -122,6 +122,19 @@ export function parseWorktreeId(worktreeId: string): { repoId: string; worktreeP
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a git error indicates the worktree is no longer tracked by git.
|
||||
* This happens when a worktree's internal git tracking is removed (e.g. via
|
||||
* `git worktree prune`) but the directory still exists on disk.
|
||||
*/
|
||||
export function isOrphanedWorktreeError(error: unknown): boolean {
|
||||
if (!(error instanceof Error)) {
|
||||
return false
|
||||
}
|
||||
const msg = (error as { stderr?: string }).stderr || error.message
|
||||
return /is not a working tree/.test(msg)
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a human-readable error message for worktree removal failures.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import type { BrowserWindow } from 'electron'
|
||||
import { ipcMain } from 'electron'
|
||||
import { execFileSync } from 'child_process'
|
||||
import { rm } from 'fs/promises'
|
||||
import type { Store } from '../persistence'
|
||||
import type { Worktree, WorktreeMeta } from '../../shared/types'
|
||||
import { listWorktrees, addWorktree, removeWorktree } from '../git/worktree'
|
||||
|
|
@ -14,7 +15,8 @@ import {
|
|||
shouldSetDisplayName,
|
||||
mergeWorktree,
|
||||
parseWorktreeId,
|
||||
formatWorktreeRemovalError
|
||||
formatWorktreeRemovalError,
|
||||
isOrphanedWorktreeError
|
||||
} from './worktree-logic'
|
||||
|
||||
export function registerWorktreeHandlers(mainWindow: BrowserWindow, store: Store): void {
|
||||
|
|
@ -150,6 +152,14 @@ export function registerWorktreeHandlers(mainWindow: BrowserWindow, store: Store
|
|||
try {
|
||||
await removeWorktree(repo.path, worktreePath, args.force ?? false)
|
||||
} catch (error) {
|
||||
// If git no longer tracks this worktree, clean up the directory and metadata
|
||||
if (isOrphanedWorktreeError(error)) {
|
||||
console.warn(`[worktrees] Orphaned worktree detected at ${worktreePath}, cleaning up`)
|
||||
await rm(worktreePath, { recursive: true, force: true }).catch(() => {})
|
||||
store.removeWorktreeMeta(args.worktreeId)
|
||||
notifyWorktreesChanged(mainWindow, repoId)
|
||||
return
|
||||
}
|
||||
throw new Error(formatWorktreeRemovalError(error, worktreePath, args.force ?? false))
|
||||
}
|
||||
store.removeWorktreeMeta(args.worktreeId)
|
||||
|
|
|
|||
Loading…
Reference in a new issue