← All runbooks in Git, CI/CD & GitOps
Runbook: Recover a Deleted Branch via Reflog
1 · Prerequisites
Confirm every item is in place before any state change.
- Refs and the refs namespace — refs/heads, refs/tags, refs/remotes
- Recovering a dropped stash — git stash list, the reflog, and the .git/logs/refs/stash file
- Working clone of the repository with the deleted branch history
- Read access to the upstream remote (origin)
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"andgit 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.reflogExpireandgit 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.
- 1Inspect the full reflog of HEAD:
git reflog --all --date=iso | head -200and 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 - 2Inspect the reflog restricted to the deleted branch (still readable by OID after deletion):
git reflog show "$BRANCH_NAME" 2>&1 || trueandgit log -g --format="%H %gs" "$BRANCH_NAME" 2>&1 | head -50 - 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) - 4Verify the candidate OID is reachable and inspect it before restoring:
git cat-file -t "$LOST_SHA"(expectcommit), thengit show --stat "$LOST_SHA" | head -40 - 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" - 6Recreate the branch from the OID:
git checkout -b "$BRANCH_NAME" "$LOST_SHA"(orgit branch "$BRANCH_NAME" "$LOST_SHA"if you do not want to switch working tree) - 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), andgit diff "main..$BRANCH_NAME" --stat - 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) - 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 10shows the expected tip commits - ✓
git diff "$BRANCH_NAME" "origin/$BRANCH_NAME"(when remote exists) shows no unexpected divergence - ✓
git reflogshows thecheckout: moving from … to $BRANCH_NAMEorbranch: 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 treeorunknown 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-leaseand 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
$ 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
$ 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 -40Pick 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
$ 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
$ 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
$ 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.