Skip to main content
RunBook Academy

← All runbooks in Git, CI/CD & GitOps

high riskdata loss risk~30 min

Runbook: Recover a Deleted Branch via Reflog

1 · Prerequisites

Confirm every item is in place before any state change.

2 · Pre-checks

Read-only diagnostic commands. If any of these don't match expected output, stop and investigate further.

  • · Confirm the branch was actually deleted and not renamed: git branch -a | grep -F "$BRANCH_NAME" and git ls-remote origin | grep -F "$BRANCH_NAME"
  • · Confirm the repository is not shallow-cloned (reflog is pruned in shallow clones): cat .git/shallow 2>/dev/null || echo not-shallow
  • · Confirm reflog has not expired: git config --get gc.reflogExpire and git config --get gc.reflogExpireUnreachable (defaults are 90 days / 30 days)
  • · Record the OID that HEAD currently points at so you can return to it if the recovery is wrong: git rev-parse HEAD | tee /tmp/pre-recovery-head.txt
  • · Stop all other agents pushing or rebasing to the repo while the recovery is in progress

3 · Procedure

Execute each step in order. Verify the expected output of a step before moving to the next.

  1. 1Inspect the full reflog of HEAD: git reflog --all --date=iso | head -200 and locate the line <SHA> HEAD@{<time>}: checkout: moving from <branch> to <branch> or <SHA> HEAD@{<time>}: commit: ... showing the last known position of $BRANCH_NAME
  2. 2Inspect the reflog restricted to the deleted branch (still readable by OID after deletion): git reflog show "$BRANCH_NAME" 2>&1 || true and git log -g --format="%H %gs" "$BRANCH_NAME" 2>&1 | head -50
  3. 3Identify the candidate tip OID: pick the most recent <SHA> in the reflog that was an explicit commit on the branch, not a merge result from a rebase. Save it: LOST_SHA=$(git reflog show "$BRANCH_NAME" --format="%H" -n 1 2>/dev/null || git log -g --format="%H" "$BRANCH_NAME" | head -1)
  4. 4Verify the candidate OID is reachable and inspect it before restoring: git cat-file -t "$LOST_SHA" (expect commit), then git show --stat "$LOST_SHA" | head -40
  5. 5Confirm the OID is on the upstream remote history if the deletion happened locally only: git fetch origin --prune && git branch -r --contains "$LOST_SHA"
  6. 6Recreate the branch from the OID: git checkout -b "$BRANCH_NAME" "$LOST_SHA" (or git branch "$BRANCH_NAME" "$LOST_SHA" if you do not want to switch working tree)
  7. 7Verify the recovered branch matches expectations: git log --oneline "$BRANCH_NAME" -n 20, git diff "origin/$BRANCH_NAME..$BRANCH_NAME" --stat (expect empty if remote branch is also gone), and git diff "main..$BRANCH_NAME" --stat
  8. 8If the remote branch was force-deleted and you need to republish: confirm with reviewers first, then git push -u origin "$BRANCH_NAME" (refuses non-fast-forward without --force-with-lease)
  9. 9If the upstream branch was force-pushed over, do not push the recovered branch over the force-pushed one without a code owner approval — the force-pushed history is the current contract for anyone who already pulled

4 · Verification

Confirm the procedure actually fixed the problem.

  • git rev-parse "$BRANCH_NAME" matches $LOST_SHA
  • git log --oneline "$BRANCH_NAME" -n 10 shows the expected tip commits
  • git diff "$BRANCH_NAME" "origin/$BRANCH_NAME" (when remote exists) shows no unexpected divergence
  • git reflog shows the checkout: moving from … to $BRANCH_NAME or branch: Created from … entry as the most recent action on this branch
  • CI pipeline re-triggered from the recovered branch SHA passes (no fatal: reference is not a tree or unknown revision)

5 · Rollback

If verification fails, undo the procedure in reverse order.

  • If the recovery pointed at the wrong OID, return to the original HEAD: git checkout "$(cat /tmp/pre-recovery-head.txt)"
  • Delete the wrong branch and retry with the next reflog entry: git branch -D "$BRANCH_NAME" && git checkout -b "$BRANCH_NAME" "<NEXT_CANDIDATE_SHA>"
  • If the remote was force-pushed over and you published the wrong OID, immediately push the correct OID with --force-with-lease and notify all consumers to re-fetch
  • If no candidate OID is reachable (reflog expired, repo was shallow, gc.pruneExpire reached): escalate to incident response — commits may still be on backup clones or fork mirrors, but the canonical branch is unrecoverable from this clone

6 · Escalation

When the runbook isn't enough, contact:

  • · Reflog has no entry for the branch AND the remote ref is also gone: data is at risk. Engage the platform team to check GitHub/GitLab reflog mirrors, fork network, and any CI workspace caches ($RUNNER_HOME/work/<repo>)
  • · Multiple agents need the recovered branch: broadcast the OID via a coordination channel before anyone pushes, or the recovery will race with the next force-push
  • · Recovery was successful but a downstream deploy already used the deleted branch SHA: treat as a deployment-against-unknown-state incident, do not just re-push the branch

A deleted branch is rarely a deleted commit. Git does not delete commits when a branch ref is removed — it just removes the label. For up to 90 days (or 30 days for unreachable commits, both configurable), the tip is reachable through the local reflog of the clone that had the branch checked out. After that window, the only remaining source is the upstream remote’s reflog, fork mirrors, or backups of CI workspaces.

This runbook is for the case where a branch was deleted by accident — locally, or on the remote — and the work needs to come back. It is not for “the branch is gone because the change was bad”. In that case, do not recover; close the PR and document why.

1. Confirm the branch is gone and not renamed

Read-only / Safe
$ BRANCH_NAME="feature/payments-replay"
git branch -a | grep -F "$BRANCH_NAME" || echo "no local branch"
git ls-remote origin | grep -F "refs/heads/$BRANCH_NAME" || echo "no remote branch"
git for-each-ref --format='%(refname) %(objectname:short) %(committerdate:iso)' refs/heads refs/remotes | grep -F "$BRANCH_NAME" || echo "no ref anywhere"

If ls-remote still shows it, the local ref is just missing — git fetch origin "refs/heads/$BRANCH_NAME:refs/remotes/origin/$BRANCH_NAME" recreates it.

2. Inspect the reflog and identify the candidate OID

Read-only / Safe
$ git reflog --all --date=iso | head -200
echo '---'
git log -g --format='%H %gs %gd' "$BRANCH_NAME" 2>&1 | head -50
echo '---'
LOST_SHA=$(git log -g --format='%H' "$BRANCH_NAME" | head -1)
echo "candidate tip: $LOST_SHA"
git cat-file -t "$LOST_SHA"
git show --stat "$LOST_SHA" | head -40

Pick the OID by inspection. The tip is the OID you want. If the reflog has multiple candidate OIDs (rebase, branch reset, force-push), the most recent commit: entry is the branch tip as last touched in this clone.

3. Recreate the branch from the OID

Read-only / Safe
$ git checkout -b "$BRANCH_NAME" "$LOST_SHA"
git log --oneline "$BRANCH_NAME" -n 20
echo '---'
echo "current HEAD: $(git rev-parse HEAD)"
echo "branch tip:   $(git rev-parse $BRANCH_NAME)"

If you cannot check out (dirty working tree, CI machine with no checkout), use git branch "$BRANCH_NAME" "$LOST_SHA" instead.

4. Verify before republishing

Read-only / Safe
$ echo '--- diff against main ---'
git diff "main..$BRANCH_NAME" --stat | head -40
echo '--- diff against remote (if exists) ---'
git fetch origin --prune
git diff "origin/$BRANCH_NAME..$BRANCH_NAME" --stat || echo 'remote branch also gone'
echo '--- expected files present ---'
git ls-tree -r --name-only "$LOST_SHA" | grep -F 'payments/replay.go' || echo 'unexpected: file missing'

If the diff is empty, you recovered the same commit the remote had. If the diff is non-empty, the remote branch was force-pushed over and you are looking at a fork. Decide explicitly which one is canonical before pushing.

5. Republish only when approved

Read-only / Safe
$ git push -u origin "$BRANCH_NAME"
# Refuses non-fast-forward without --force-with-lease.
# If you need to overwrite a force-pushed remote, use --force-with-lease:
# git push --force-with-lease origin "$BRANCH_NAME":"$BRANCH_NAME"

Verification

git rev-parse "$BRANCH_NAME" equals the recorded $LOST_SHA. The branch tip appears in git log --oneline -n 10 with the expected commit subject. git diff "main..$BRANCH_NAME" --stat matches the last-known-good diff for that branch. If the branch was used to deploy, the recovered SHA is the same SHA the deployment was performed against (compare against CI logs, not just against origin/$BRANCH_NAME).

Rollback

If the recovery pointed at the wrong OID, the original HEAD is still in /tmp/pre-recovery-head.txt: git checkout "$(cat /tmp/pre-recovery-head.txt)" returns to a known state. Delete the bad branch with git branch -D "$BRANCH_NAME" and pick the next reflog entry as the candidate OID.

If the recovery was pushed and a downstream pipeline already consumed it, the correct rollback is to push the correct OID with --force-with-lease and notify every consumer to re-fetch. There is no client-side fix for a pipeline that ran against the wrong SHA — that is a deployment incident, not a Git incident.

References

  1. git-reflog(1) — the local history of ref updates
  2. git-branch(1) — recreating and deleting branches
  3. git-config(1) — gc.reflogExpire and gc.reflogExpireUnreachable
  4. Pro Git §10.3 — reflog as a safety net
  5. GitHub Docs — Restoring a deleted branch