import type { GitWorktreeInfo } from './types' export const LOCKED_WORKTREE_REMOVAL_PREFIX = 'Worktree is locked by Git.' export type WorktreeForceDeleteReason = 'dirty' | 'orphan-directory' | 'missing-registration' export function createLockedWorktreeRemovalError(lockReason?: string): Error { const reason = lockReason?.trim() return new Error( reason ? `${LOCKED_WORKTREE_REMOVAL_PREFIX} Lock reason: ${reason}. Run git worktree unlock from its repository, then retry deletion.` : `${LOCKED_WORKTREE_REMOVAL_PREFIX} Run git worktree unlock from its repository, then retry deletion.` ) } export function assertWorktreeUnlockedForRemoval( worktree: Pick | undefined ): void { if (worktree?.locked) { throw createLockedWorktreeRemovalError(worktree.lockReason) } } export function isLockedWorktreeRemovalError(error: string): boolean { return ( error.includes(LOCKED_WORKTREE_REMOVAL_PREFIX) || error.includes('cannot remove a locked working tree') ) } export function getLockedWorktreeRemovalReason(error: string): string | null { const prefixIndex = error.indexOf(`${LOCKED_WORKTREE_REMOVAL_PREFIX} Lock reason: `) if (prefixIndex === -1) { return null } const reasonStart = prefixIndex + `${LOCKED_WORKTREE_REMOVAL_PREFIX} Lock reason: `.length const recoverySuffix = '. Run git worktree unlock from its repository, then retry deletion.' const suffixIndex = error.indexOf(recoverySuffix, reasonStart) const reason = error.slice(reasonStart, suffixIndex === -1 ? undefined : suffixIndex).trim() return reason || null } const FORMATTED_DIRTY_WORKTREE_REMOVAL_PATTERN = /Failed to delete worktree at [^\n]*\.\s*(?:(?:[MADRCUT][ MADRCUT]| [MADRCUT]|\?\?)\s+\S)/ export function classifyWorktreeForceDeleteReason( error: string, force = false ): WorktreeForceDeleteReason | null { if (isLockedWorktreeRemovalError(error)) { // Why: a Git lock can represent an external safety contract. It must be // unlocked explicitly rather than folded into Orca's dirty-file force path. return null } if (force) { return null } if (error.includes('Worktree is no longer registered with Git but its directory remains')) { return 'orphan-directory' } if ( error.includes('Worktree is no longer registered with Git and its directory is already gone') ) { return 'missing-registration' } if ( error.includes('Worktree has uncommitted or untracked changes') || error.includes('contains modified or untracked files') || FORMATTED_DIRTY_WORKTREE_REMOVAL_PATTERN.test(error) ) { return 'dirty' } return null }