All posts
·13 min read· recovery· reflog· branches· fsck· undo

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:

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:

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:

SettingDefaultWhat it controls
gc.reflogExpire90 daysHow long reflog entries are kept for reachable commits
gc.reflogExpireUnreachable30 daysHow long reflog entries are kept for unreachable commits
gc.pruneExpire2 weeksGrace 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:

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:

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

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.