How to recover a deleted Git branch (even after git branch -D)
Deleted a Git branch by mistake? The commits live on for ~90 days. Here are the exact reflog and git fsck commands to bring a deleted branch back, with realistic walkthroughs.
It is 6 p.m. on a Friday. You ran git branch -D feature/payments. The terminal printed a single line and gave you the prompt back. Three hours of work — a payments webhook, a retry queue, two tests — gone. You never pushed. You never opened a PR. You just deleted the branch.
You can get every commit back in about a minute.
This post walks through the recovery, step by step, with the exact commands. It also covers the fallback path for when the branch was never checked out, when the recovery window closes, and how to make sure this never costs you more than a minute again.
What git branch -D actually does
A branch in Git is not a copy of your code. It is a single line in a file at .git/refs/heads/<branch-name> that holds one SHA — the SHA of the latest commit on that branch. That is the entire branch.
When you run git branch -D feature/payments, Git deletes that one line. The commits themselves — the actual snapshots of your files — are not touched. They sit in .git/objects/, waiting.
Run the man git-branch page and you can see this confirmed: the -D flag is "shortcut for --delete --force". It removes the ref. It does not remove objects.
This matters because Git has two separate things:
- Refs — pointers like
main,feature/payments,HEAD. Cheap to delete, cheap to recreate. - Objects — the actual commit, tree, and blob data. These live in
.git/objects/and get cleaned up bygit gcon a long schedule.
Deleting a branch only removes a ref. To lose the commits, you have to wait for garbage collection — and by default that is at least 30 days for unreachable commits, and 90 days for commits the reflog can still reach. We will come back to those numbers.
For now, the takeaway: the commits are still there. You just need to find their SHAs and point a new branch at them.
The fast path: git reflog
The reflog is a journal of every move HEAD has made on your machine. Every commit, checkout, merge, reset, rebase step — Git writes a line into the reflog with the old SHA, the new SHA, and a short reason like commit: or checkout:.
The reflog is local to your clone. It is not pushed. It is not part of the repo. See the git reflog docs for the full specification.
Most of the time, the deleted branch was the branch you were just on. HEAD sat at its tip. That means the reflog has the SHA you need.
Step 1 — Look at the reflog
git reflog
You will see something like:
b4c8e2f HEAD@{0}: checkout: moving from feature/payments to main
b4c8e2f HEAD@{1}: commit: feat: add retry queue for webhook delivery
9f4e2c8 HEAD@{2}: commit: feat: payment webhook handler
6c1a5e3 HEAD@{3}: commit: feat: payment scaffold
3d8f2b1 HEAD@{4}: checkout: moving from main to feature/payments
3d8f2b1 HEAD@{5}: commit: chore: bump stripe sdk
Read top-down as "what HEAD just did, in reverse order". The most recent action is HEAD@{0} — you switched to main (after which you deleted the branch). The line right above it, HEAD@{1}, is the last commit on the deleted branch. That SHA — b4c8e2f — is the tip of feature/payments.
Step 2 — Recreate the branch at that SHA
git branch feature/payments b4c8e2f
That is it. The branch is back. All three commits are reachable again. Run git log --oneline feature/payments and you will see exactly what you had.
If you prefer to check out the branch as well:
git checkout -b feature/payments b4c8e2f
Step 3 — Sanity check
git log --oneline feature/payments -5
Confirm the commits you expected are there. If something looks off, run git reflog again — your new branch creation will be the new HEAD@{0} and nothing has been lost. The reflog is append-only. You can poke around as much as you want.
When was the branch deleted? Filtering the reflog
For a recent deletion, the lines you need are near the top of git reflog. For a branch you deleted yesterday, you may need to scroll.
A useful filter — search the reflog for the branch name:
git reflog | grep payments
Or use the per-ref reflog, which Git keeps separately in .git/logs/refs/heads/<branch>:
git reflog show feature/payments
Heads-up: once you delete a branch, its per-ref log file is removed too. So git reflog show feature/payments will not work after deletion. But the global reflog (the one you get from git reflog with no arguments) still has every line where HEAD sat on that branch. That is what grep searches.
If you ran multiple commits on the deleted branch, you do not need the tip SHA alone. Any SHA from the branch will do — once you have one commit, git log <sha> shows the full ancestry.
The fallback path: git fsck --lost-found
There is one case the reflog can not handle: the deleted branch was never checked out locally. For example:
- You fetched a remote branch with
git fetch, then rangit branch -D origin/feature/payments. - A teammate created a branch on your machine via a script that never moved
HEAD. - You used
git update-refdirectly.
In these cases, HEAD never moved to the branch. The reflog has no record. You need another tool: git fsck.
git fsck --lost-found
fsck walks the entire .git/objects/ directory and reports any commit, tree, or blob that no ref can reach. These are called dangling objects. The output looks like:
Checking object directories: 100% (256/256), done.
dangling commit b4c8e2f0a1d3f7c9e2b8d4a6f1c3e5b7d9a2f4c6
dangling commit 9f4e2c83a7b1d5f9e3c7a1b5d9f3e7c1b5d9f3e7
dangling blob 2d3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a
Each dangling commit is a commit no branch points at. One of them is your deleted branch tip. To find it, inspect each:
git show b4c8e2f --stat
You will see the commit message, author, date, and the files it touched. When you find the right one, plant a branch at it:
git branch feature/payments b4c8e2f
For many dangling commits, scripting helps:
git fsck --lost-found | grep "dangling commit" | awk '{print $3}' | \
xargs -I{} git log -1 --format="%h %ad %s" {}
That prints a one-line summary of every dangling commit. Scan for the message you remember writing.
A complete walkthrough
Here is what the whole flow looks like end-to-end. Suppose you have just done this:
git checkout main
git branch -D feature/payments
# (panic)
Recovery, in five commands:
# 1. Find the tip of the deleted branch
git reflog | grep -E "checkout: moving (from feature/payments|to feature/payments)|commit.*payments"
# 2. Pick the SHA right before the "moving from feature/payments to main" line
# That is the last commit on the deleted branch
# 3. Recreate the branch
git branch feature/payments <sha>
# 4. Verify
git log --oneline feature/payments -10
# 5. Push so this can never happen again
git push -u origin feature/payments
Total time: under a minute, once you know the steps.
When recovery becomes impossible
Two things can make recovery fail. Both involve garbage collection — the background process that actually removes unreachable objects.
The defaults are documented in git gc and git reflog expire:
| Setting | Default | What it controls |
|---|---|---|
gc.reflogExpire | 90 days | How long reflog entries are kept for reachable commits |
gc.reflogExpireUnreachable | 30 days | How long reflog entries are kept for unreachable commits |
gc.pruneExpire | 2 weeks | Grace period for objects with no ref before they are pruned |
The practical rule: if you deleted a branch in the last 30 days and have not aggressively run git gc --prune=now, you can almost always recover it. Past that window, recovery depends on whether gc actually ran. gc is not a daemon — it runs only when triggered by certain commands (a heavy git fetch, a git push, or an explicit git gc). On a quiet repo, commits can persist for weeks or months past the expire window.
You can check your repo's settings:
git config gc.reflogExpire
git config gc.reflogExpireUnreachable
You can also extend them. On personal machines I run with:
[gc]
reflogExpire = 365.days
reflogExpireUnreachable = 365.days
The disk cost is small — a year of normal Git activity is a few megabytes in .git/objects/. The peace of mind is large.
If you have run git gc --prune=now or git reflog expire --expire-unreachable=now since the deletion, the objects may genuinely be gone. There is no recovery from a successful prune. This is rare in practice — most people never run gc by hand.
Prevention — three habits that make this a non-event
Recovery in a minute is fine. Never needing recovery is better.
Push early, even unfinished work. As soon as a feature branch has anything worth keeping, push it. A branch that exists on origin cannot be deleted by a local git branch -D — only by git push origin --delete <branch>, which is harder to type by accident.
git push -u origin feature/payments
After this, your local git branch -D feature/payments removes only the local ref. The remote branch and its commits are safe.
Tag before any destructive operation. Tags are refs too, and like branches they keep commits reachable. Before you delete, rebase, or reset, drop a tag:
git tag pre-cleanup feature/payments
git branch -D feature/payments
# later, if you regret it:
git branch feature/payments pre-cleanup
git tag -d pre-cleanup
Alias the safety net. A common pair I keep in .gitconfig:
[alias]
rl = reflog --date=relative
oops = !git reset --hard HEAD@{1}
bsave = !sh -c 'git tag backup/$(date +%s) $1' -
git rl gives a readable reflog. git oops reverses your last HEAD move, whatever it was. git bsave <branch> creates a timestamped tag at any branch so you can find it later.
What about deleted remote branches?
A remote branch is just a ref on the server — refs/heads/<branch> in the remote repo, mirrored locally as refs/remotes/origin/<branch>. Recovery rules:
If you deleted with git push origin --delete <branch>, the remote ref is gone. But:
- Anyone with a local clone that fetched the branch still has it under
refs/remotes/origin/<branch>until they next prune. - The remote's reflog (on the server) may still have the SHA. On GitHub, GitLab, and Bitbucket, deleted branches can usually be restored from the web UI for a window (GitHub keeps them for ~90 days under PR "Restore branch" buttons).
- Any clone with the SHA in its reflog can re-push:
git push origin <sha>:refs/heads/<branch>.
If the branch was deleted on the server by someone else, your local refs/remotes/origin/<branch> will be cleaned up the next time you run git fetch --prune (or git remote prune origin). Until then, you still have the SHA locally, and you can re-create the branch from it.
Recovery from the server side depends on the host. GitHub's branch restoration UI is the fastest path when it applies. For self-hosted Git, ask whoever runs the server — most have backups.
Three common myths
Myth 1: "git branch -D deletes the commits." Wrong. It deletes only the ref. The commits remain in .git/objects/ until garbage collection. This is the entire reason recovery works.
Myth 2: "If git status shows clean, the branch is permanently gone." Wrong. git status only describes the working tree and index. It does not look at objects or the reflog. A deleted branch is invisible to status but still recoverable.
Myth 3: "You need a backup to recover a deleted branch." Mostly wrong. For 99% of deletions on a normal local repo within the last 30 days, the reflog and git fsck --lost-found are enough. Backups are useful when garbage collection has actually run, or when the entire repo is lost — but for branch deletion alone, Git's own object database is the backup.
A note on git switch and git checkout
Modern Git (since version 2.23 in August 2019) split the overloaded git checkout command into two clearer commands: git switch (for changing branches) and git restore (for changing files). All recovery commands in this post work the same way with either old or new syntax. If you prefer the modern style:
# Recreate and check out a branch from a SHA
git switch -c feature/payments b4c8e2f
# Old-style equivalent (also still works)
git checkout -b feature/payments b4c8e2f
Both produce the same result. Use whichever you have in your muscle memory.
What about hooks and per-branch state?
A few things tied to a branch are not recovered when you rebuild the branch from a SHA:
- Upstream tracking. Re-set it explicitly with
git branch --set-upstream-to=origin/feature/paymentsorgit push -u origin feature/payments. - PR state on GitHub/GitLab/Bitbucket. A closed-because-deleted PR usually has a "Restore branch" button that re-pushes the SHA. Use the host UI if it is still available — it preserves PR comments, reviews, and CI history.
- Local stashes that referenced the branch. Stashes are independent of branches. Run
git stash listto see any that survived the deletion. - Worktrees. If you had a
git worktreepointing at the deleted branch, the worktree directory still exists but itsHEADis broken. Rungit worktree repairafter recreating the branch.
None of these blocks the recovery itself — the commits come back regardless — but they are worth checking before you treat the incident as fully closed.
Try it without the panic
The first time you recover a deleted branch, the muscle memory comes with a panic flavour attached. The second time, it is a sixty-second chore.
Open the deleted branch recovery scenario in the live runner — a seeded repo where a branch has been deleted and three hours of work is "at risk". Walk through the reflog and recovery in a real terminal, with nothing real at risk. The first deliberate recovery is what makes the next accidental one feel routine.
What to read next
- git reflog: the undo button you didn't know you had — a deeper look at the reflog itself and the other things it saves you from.
- Choosing a Git workflow: a decision guide for real teams — once your branch is back, where should it live? Push-early habits are easier inside a workflow that supports them.
- The Recovering Deleted Branches lesson in the sidebar opens a live terminal with a real deleted branch waiting to be brought back.
- The Reflog — Undo Anything lesson covers the broader command surface — every place the reflog saves you, not just branch deletion.
If you remember one thing from this post: deleting a branch in Git is cheap, and bringing it back is also cheap. The commits do not disappear when the ref does. They sit and wait. You just have to ask for them.