{"record":{"id":"233e9ce1ee9cd185","repo":"stablyai/orca","slug":"cannot-force-delete-local-branch-branchname-w","errorCode":null,"errorMessage":"Cannot force-delete local branch \"${branchName}\" without the commit Git preserved.","messagePattern":"Cannot force-delete local branch \"(.+?)\" without the commit Git preserved\\.","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/main/git/worktree.ts","lineNumber":1388,"sourceCode":"    gitExecFileAsync(args, gitExecOptions(cwd, options))\n  )\n  return true\n}\n\nexport async function forceDeleteLocalBranch(\n  repoPath: string,\n  branchName: string,\n  expectedHead: string,\n  runGit: (args: string[], cwd: string) => Promise<{ stdout: string; stderr: string }> = (\n    args,\n    cwd\n  ) => gitExecFileAsync(args, { cwd })\n): Promise<void> {\n  if (!branchName || branchName.includes('\\0')) {\n    throw new Error('Invalid branch name')\n  }\n  if (!expectedHead) {\n    throw new Error(\n      `Cannot force-delete local branch \"${branchName}\" without the commit Git preserved.`\n    )\n  }\n  if (await isLocalBranchCheckedOut(repoPath, branchName, runGit)) {\n    throw new Error(`Local branch \"${branchName}\" is checked out in another worktree.`)\n  }\n  // Why: stale toast actions must not delete a branch that moved; `update-ref -d` deletes only if the ref still == expectedHead.\n  try {\n    await runGit(['update-ref', '-d', `refs/heads/${branchName}`, expectedHead], repoPath)\n  } catch {\n    throw new Error(\n      `Local branch \"${branchName}\" changed after the workspace was deleted. Review it before deleting it.`\n    )\n  }\n  if (await isLocalBranchCheckedOut(repoPath, branchName, runGit)) {\n    try {\n      await runGit(['update-ref', `refs/heads/${branchName}`, expectedHead, ''], repoPath)\n    } catch (restoreError) {","sourceCodeStart":1370,"sourceCodeEnd":1406,"githubUrl":"https://github.com/stablyai/orca/blob/1136503c6a231a16dce8f921f6fadb63d181e8db/src/main/git/worktree.ts#L1370-L1406","documentation":"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`.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Ensure every caller resolves and passes the branch's current HEAD SHA before invoking forceDeleteLocalBranch.","If the SHA is genuinely unavailable (worktree already gone), do not call this function — surface a manual-review prompt to the user instead.","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."],"exampleFix":"// before\nawait forceDeleteLocalBranch(repoPath, branchName, undefined)\n\n// after\nconst head = await resolveBranchHead(repoPath, branchName)\nif (!head) {\n  toast.error('Cannot determine branch HEAD; review before deleting.')\n  return\n}\nawait forceDeleteLocalBranch(repoPath, branchName, head)","handlingStrategy":"validation","validationCode":"function hasResolvedHead(head: unknown): head is string {\n  return typeof head === 'string' && /^[0-9a-f]{40}$/i.test(head)\n}","typeGuard":"function hasResolvedHead(head: unknown): head is string {\n  return typeof head === 'string' && /^[0-9a-f]{40}$/i.test(head)\n}","tryCatchPattern":null,"preventionTips":["Capture branch HEAD at the same instant you queue the delete action, not lazily on replay.","If HEAD cannot be resolved, prompt the user for manual review instead of calling through.","Never pass undefined or empty string as expectedHead — the guard refuses it by design."],"tags":["git","branch","validation","optimistic-concurrency","programmer-error"],"backgroundTag":null,"analyzedSha":"1136503c6a231a16dce8f921f6fadb63d181e8db","analyzedAt":"2026-08-12T23:15:58.167Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}