Skip to main content
RunBook Academy

Git, CI/CD & GitOpsVIII · BranchingBranching

Branch deletion and recovery — git branch -d, git branch -D, and the reflog resurrection path

Intermediate⏱ ~22 mingit

What you'll learn

  • Distinguish -d (safe delete, refuses if unmerged) from -D (force delete, orphans the tip)
  • Identify the window during which a deleted branch can be resurrected via the reflog
  • Resurrect a deleted branch by creating a new branch at the orphaned commit OID
  • Recognise the role of git gc in making orphaned commits unrecoverable
  • Use git reflog and git fsck --lost-found to locate orphaned commits

Prerequisites

Verified against Git 2.55.x teaching target; 2.40+ minimum · GitHub Actions continuous service; Aug 2026 documentation baseline · Argo CD v3.5.x teaching target; v3.0+ minimum · Flux v2.9.x · Sigstore Cosign v3.1.x · SLSA v1.2 · OCI Distribution Specification v1.1 · Git LFS v3.7.1 · Kubernetes (cross-course target) 1.36.x

Not yet marked complete on this device.

Deleting a branch is the most dangerous routine operation in Git. It is dangerous because the failure mode is silent: there is no “are you sure?” prompt by default, no trash can to recover from, no undo button. The command removes a single file under .git/refs/heads/ and the operation is done. The commits that were only reachable from that file become orphans; they stay in the object store until garbage collection runs, and they stay reachable from any clone that previously fetched the branch, but the local branch is gone. The safety net is the reflog: a 90-day journal of every ref movement, which lets an engineer who realises their mistake within the window resurrect the branch by recreating it at the recorded OID.

The safe delete: -d

git branch -d <name> is the safe delete. Git walks the commit graph from the branch tip and asks: is this commit reachable from any other ref? If yes, the branch ref can be safely removed because the commits will continue to be reachable from the other ref. If no, Git refuses:

git branch -d feature/iam-rotation
# error: The branch 'feature/iam-rotation' is not fully merged.
# If you are sure you want to delete it, run 'git branch -D feature/iam-rotation'.

The reachability check considers every ref: every local branch, every remote-tracking ref, every tag. If any of them can reach the branch tip, the delete proceeds. The check is cheap — Git does not need to walk the entire graph; it stops at the first ref that can reach the tip.

# Safe delete succeeds when the branch is merged
git branch -d bugfix/cert-renewal
# Deleted branch bugfix/cert-renewal (was 4d2c8e0).

# Safe delete refuses when the branch has unique commits
git branch -d feature/iam-rotation
# error: The branch 'feature/iam-rotation' is not fully merged.

The safe delete is the right command for routine cleanup. In production, automated scripts that prune merged branches should use -d and inspect the exit code; a non-zero exit indicates a branch whose tip is not reachable from any other ref, and the script should log the refusal rather than escalating to -D.

The force delete: -D

git branch -D <name> is the force delete. It removes the branch ref without consulting the commit graph. Any commits that were only reachable from this branch become orphans — they are still in the object store, but they have no ref pointing at them and are candidates for garbage collection.

# Force delete, no questions asked
git branch -D feature/iam-rotation
# Deleted branch feature/iam-rotation (was 9f3c1d7).

The flag is uppercase -D to signal “this is a sharp tool”. The same syntax also accepts the long form --delete --force but -D is the universal abbreviation.

flowchart LR
    A["branch tip reachable?"] -->|"yes"| B["git branch -d succeeds"]
    A -->|"no"| C["git branch -d refuses"]
    C -->|"force"| D["git branch -D succeeds"]
    D --> E["tip becomes orphan"]
    E -->|"reflog still has it"| F["recoverable for 90 days"]
    E -->|"reflog expired + gc ran"| G["unrecoverable"]

The diagram shows the decision flow. -d is the safe path that refuses unsafe deletes; -D is the path that bypasses the check and accepts the orphan state.

The reflog as a safety net

The reflog is a per-ref journal of every ref movement. For branches, every commit that HEAD moved to is recorded; for remote-tracking refs, every fetch is recorded. The reflog is the mechanism that makes recovery from an accidental -D possible.

# View the reflog for HEAD (every commit HEAD has pointed at)
git reflog
# 9f3c1d7 HEAD@{0}: commit: rotate iam keys
# 6f4e5a6 HEAD@{1}: checkout: moving from main to feature/iam-rotation
# 8a3f9d2 HEAD@{2}: commit: bump terraform module
# 4d2c8e0 HEAD@{3}: commit: rotate iam keys (initial)

When a branch is deleted, the OID is still in the reflog for the default 90 days. Any clone that previously fetched the branch will have the same OID in its reflog (or in its remote-tracking ref’s reflog), and the commits are reachable from any clone that fetched them.

The reflog entries for a deleted branch can be found by walking git reflog and looking for the branch name in the action description:

# Find the OIDs that any branch tip pointed at in the last 30 days
git reflog --all | grep 'feature/iam-rotation'
# 9f3c1d7 feature/iam-rotation@{0}: commit: rotate iam keys
# 6f4e5a6 feature/iam-rotation@{1}: branch: Created from main

The output includes both the OIDs and the timestamps. The most recent commit’s OID is the tip of the deleted branch.

Resurrecting a deleted branch

Recovery is a two-step process: find the OID, create a new branch at it.

# Step 1: locate the orphaned tip
git reflog --all | grep 'feature/iam-rotation'
# 9f3c1d7 feature/iam-rotation@{0}: commit: rotate iam keys

# Step 2: resurrect the branch at the recorded OID
git branch feature/iam-rotation 9f3c1d7
# Restored branch 'feature/iam-rotation'

# Step 3: restore the upstream configuration
git branch --set-upstream-to=origin/feature/iam-rotation

The resurrected branch is identical to the original at the moment of deletion: same tip, same history, same commits. The reflog entry feature/iam-rotation@{0} becomes the new feature/iam-rotation@{1} (the new creation), and the deleted branch’s history is preserved in the reflog.

If the reflog has expired (or the work was on a clone whose reflog has expired), the next-best path is git fsck:

# Find dangling commits (orphans that no ref points at)
git fsck --lost-found
# dangling commit 9f3c1d7
# dangling commit 6f4e5a6

git fsck --lost-found writes the orphaned objects to .git/lost-found/commit/ and .git/lost-found/other/. The engineer can read the commit objects and identify the relevant ones by inspecting their messages with git cat-file -p <oid>.

The role of git gc

Garbage collection is the mechanism that actually frees the disk space used by orphaned commits. Without git gc, orphans accumulate forever (they are not deleted just because no ref points at them). With git gc, orphans are pruned when their reflog entry has expired.

# Manually run gc with the default prune window
git gc

# Aggressive: prune everything not reachable now
git gc --prune=now

# Conservative: only prune objects older than 30 days
git gc --prune=30.days

The default behaviour is to keep unreachable objects for 2 weeks (the --prune default) and to expire reflog entries older than 90 days (reachable) or 30 days (unreachable). The combination means:

  • An orphan that was created today is recoverable via the reflog for at least 30 days, possibly 90.
  • An orphan that was created 90+ days ago and whose reflog entry has expired is a candidate for the next git gc --prune=now and may already be gone.

In production, repositories with aggressive gc policies (some hosted Git services run gc weekly) have a shorter recovery window than self-hosted repositories with default policies. An engineer who needs a 30-day recovery window should explicitly extend the reflog retention before the window closes.

Production discipline

  1. Use -d for every routine delete. The safe delete is the default; -D is the exception. The exit code of -d is the ground truth: a non-zero exit means the branch had unique commits and was preserved.
  2. Log orphaned OIDs on -D. A -D should never be silent. The cleanup script should write the deleted tip OIDs to a log so the team can resurrect the branch if the decision is reversed.
  3. Extend the reflog before the window closes. A high-stakes branch that was deleted in error can be protected by running git reflog expire --expire=180.days --all on every clone that may have the work.
  4. Use git fsck --lost-found for post-reflog recovery. When the reflog has expired, the orphaned commits may still be in the object store until the next git gc. git fsck --lost-found finds them and writes them to a recoverable location.
  5. Document the recovery window in the team’s runbook. “If you deleted a branch by mistake within the last 90 days, use the reflog. After 90 days, recovery is not guaranteed.”

Cross-course references

  • Ansible for Production Sysadmins - Part XXXVIII (Review) uses git branch -d for routine cleanup and reserves -D for branches that have been explicitly confirmed as abandoned in a team sync.
  • GitOps with Argo CD - Part IV (AppSources) discusses the recovery window for application source branches: an Argo Application that points at a deleted branch enters a degraded state, and the recovery is to recreate the branch via the reflog of a clone that previously fetched it.
  • Terraform for Production Sysadmins - Part XII (State) draws the analogy between git branch -D and terraform state rm: both are operations that orphan an object and require explicit verification before they are run in production.

Quiz

Knowledge check · 4 questions

  1. Q1. An engineer accidentally runs `git branch -D feature/iam-rotation` on a branch with 7 unique commits. How long do they have to resurrect the branch before the commits become unrecoverable?

  2. Q2. `git branch -d <name>` refuses to delete a branch whose tip commit is not reachable from any other ref, while `git branch -D <name>` deletes it unconditionally.

  3. Q3. How do you resurrect a deleted branch using the reflog, in two steps?

  4. Q4. Diagnose a deletion incident where a feature branch with 14 unique commits was force-deleted, and recommend a recovery plan and a process fix.

    A nightly cleanup script ran `git branch -D` on every branch that had not been touched in 90 days. The script deleted `feature/iam-rotation`, which had 14 unique commits that were never merged into main. The engineer who owned the branch discovered the deletion 4 days later when they tried to push a follow-up commit. The team's policy is that unreviewed work must be preserved for 180 days. The on-call is investigating.

Passing score: 75%. Answers are checked in this browser.