stablyai/orca · error

Cannot force-delete local branch "${branchName}" without the

Error message

Cannot force-delete local branch "${branchName}" without the commit Git preserved.

What it means

Thrown by forceDeleteLocalBranch when expectedHead is falsy. The function deletes via `update-ref -d refs/heads/<name> <expectedHead>`, an optimistic-concurrency delete that only succeeds if the ref still points at expectedHead. Without expectedHead the operation cannot be made safe — a stale toast action could delete a branch that has since received new commits. This guard refuses to proceed rather than fall back to an unconditional `branch -D`.

Source

Thrown at src/main/git/worktree.ts:1388

    gitExecFileAsync(args, gitExecOptions(cwd, options))
  )
  return true
}

export async function forceDeleteLocalBranch(
  repoPath: string,
  branchName: string,
  expectedHead: string,
  runGit: (args: string[], cwd: string) => Promise<{ stdout: string; stderr: string }> = (
    args,
    cwd
  ) => gitExecFileAsync(args, { cwd })
): Promise<void> {
  if (!branchName || branchName.includes('\0')) {
    throw new Error('Invalid branch name')
  }
  if (!expectedHead) {
    throw new Error(
      `Cannot force-delete local branch "${branchName}" without the commit Git preserved.`
    )
  }
  if (await isLocalBranchCheckedOut(repoPath, branchName, runGit)) {
    throw new Error(`Local branch "${branchName}" is checked out in another worktree.`)
  }
  // Why: stale toast actions must not delete a branch that moved; `update-ref -d` deletes only if the ref still == expectedHead.
  try {
    await runGit(['update-ref', '-d', `refs/heads/${branchName}`, expectedHead], repoPath)
  } catch {
    throw new Error(
      `Local branch "${branchName}" changed after the workspace was deleted. Review it before deleting it.`
    )
  }
  if (await isLocalBranchCheckedOut(repoPath, branchName, runGit)) {
    try {
      await runGit(['update-ref', `refs/heads/${branchName}`, expectedHead, ''], repoPath)
    } catch (restoreError) {

View on GitHub (pinned to 1136503c6a)

Solutions

  1. Ensure every caller resolves and passes the branch's current HEAD SHA before invoking forceDeleteLocalBranch.
  2. If the SHA is genuinely unavailable (worktree already gone), do not call this function — surface a manual-review prompt to the user instead.
  3. Re-derive expectedHead with `git rev-parse refs/heads/<name>` against the live repo before the call, accepting that a race is then possible but at least the delete is bounded.

Example fix

// before
await forceDeleteLocalBranch(repoPath, branchName, undefined)

// after
const head = await resolveBranchHead(repoPath, branchName)
if (!head) {
  toast.error('Cannot determine branch HEAD; review before deleting.')
  return
}
await forceDeleteLocalBranch(repoPath, branchName, head)
Defensive patterns

Strategy: validation

Validate before calling

function hasResolvedHead(head: unknown): head is string {
  return typeof head === 'string' && /^[0-9a-f]{40}$/i.test(head)
}

Type guard

function hasResolvedHead(head: unknown): head is string {
  return typeof head === 'string' && /^[0-9a-f]{40}$/i.test(head)
}

Prevention

When it happens

Trigger: A 'delete branch' toast action whose serialized payload lost the head SHA; calling forceDeleteLocalBranch from new code that omitted the expectedHead argument; the workspace was deleted before the preserved commit SHA was captured.

Common situations: Stale toast action replayed after an app restart that didn't re-hydrate the head SHA; a refactor that introduced a call path forgetting to thread branchHead through.

Related errors


AI-assisted analysis of stablyai/orca@1136503c6a (2026-08-12). Data as JSON: /api/errors/233e9ce1ee9cd185. Report an issue: GitHub.