TechEarl

How to Recover Lost Commits with git reflog

Think you lost work to a bad reset, rebase, or deleted branch? You almost certainly did not. git reflog is Git's safety net, and here is how to use it to get your commits back.

Ishan Karunaratne⏱️ 9 min readUpdated
Share thisCopied
Using git reflog to find and recover lost commits after a bad reset, rebase, or branch delete

If a bad git reset, a botched rebase, or a deleted branch seems to have wiped out your commits, take a breath: the work is almost always still there. Git keeps a log of every place HEAD has pointed, called the reflog. Run git reflog, find the line just before things went wrong, copy its commit hash, and bring it back with git reset --hard <hash> (or, more safely, git checkout <hash>). The commit was never deleted, only unreferenced.

That is the whole rescue in one paragraph. The rest of this article explains what the reflog actually is, why your commits survive, and how to handle each of the common ways people think they lost work.

What is the reflog?

The reflog ("reference log") is a private, local diary of where your branch tips and HEAD have pointed over time. Every commit, checkout, reset, merge, rebase, and pull updates a reference, and Git records the move. So even when a commit is no longer reachable from any branch, the reflog still remembers the hash it used to point to.

Here is what it looks like:

bash
git reflog
Recovering a lost commit using git reflog and git reset
git reflog finds the commit a hard reset dropped, and reset --hard restores it.
text
e3a1f0c (HEAD -> main) HEAD@{0}: reset: moving to HEAD~3
9b7d2a4 HEAD@{1}: commit: add password reset endpoint
1c8e5f9 HEAD@{2}: commit: wire up email templates
4f6b0d3 HEAD@{3}: commit: scaffold auth routes
a2c9e88 HEAD@{4}: checkout: moving from develop to main

Each line is one move of HEAD. The left column is the commit hash, HEAD@{n} is how many moves ago it was (0 is the most recent), and the text after the colon is the action that caused the move. Reading top to bottom is reading backward in time.

Two things make the reflog the right tool for recovery:

  • It is local and per-clone. It lives in .git/logs/ and is never pushed or fetched. Your reflog is yours; a teammate's reset does not show up in it, and your recovery does not need the network.
  • It records moves that history rewriting hides. A rebase or a reset --hard changes which commits are reachable, but the reflog still has the old hashes, which is exactly what you need to undo the damage.

For the bigger picture of how commits, branches, and HEAD fit together, the Git for Beginners guide is the place to start, and Git branching explained covers what a branch pointer really is.

Why your commits are not actually gone

A commit in Git is an immutable object stored by its hash. A branch is just a movable label pointing at one commit. When you "lose" a commit, what usually happened is the label moved off it, leaving the commit with no name on it. The object itself is untouched in your .git database.

Git only permanently removes unreachable objects when garbage collection runs, and git gc keeps anything younger than its grace period. The split that matters here: reflog entries for commits no longer reachable from any branch default to 30 days (gc.reflogExpireUnreachable), while reflog entries for commits still on a branch default to 90 days (gc.reflogExpire). The commits you recover after a bad reset, rebase, or branch delete are the unreachable ones, so 30 days is the figure that governs recovery. In practice that still means you have weeks, not seconds, to recover. The reflog is what keeps those commits reachable during that window.

So the recovery game is always the same: find the hash the commit still has, then point a reference back at it.

Recovering after a bad git reset

This is the classic one. You meant to unstage a file or move back one commit and instead ran something like:

bash
git reset --hard HEAD~3

Three commits vanish from git log. They are not gone. Look at the reflog:

bash
git reflog
text
e3a1f0c (HEAD -> main) HEAD@{0}: reset: moving to HEAD~3
9b7d2a4 HEAD@{1}: commit: add password reset endpoint
1c8e5f9 HEAD@{2}: commit: wire up email templates
4f6b0d3 HEAD@{3}: commit: scaffold auth routes

HEAD@{1} (9b7d2a4) is the commit you were on right before the reset. Point your branch back at it:

bash
git reset --hard 9b7d2a4

git log now shows all three commits again. You can also use the relative form, git reset --hard HEAD@{1}, but copying the explicit hash is harder to get wrong.

A word of caution: reset --hard discards any uncommitted changes in your working tree, which is why it bit you the first time. If you have local edits you want to keep, stash them first (how to use git stash) before resetting. For a full rundown of the safer alternatives to a hard reset, see how to discard local changes in Git and git reset vs git revert.

Recovering after a botched rebase

Interactive rebase rewrites a run of commits into new objects with new hashes. If you squashed or dropped the wrong commit, or hit a conflict and bailed out in a confused state instead of resolving the merge conflicts, the original chain is still in the reflog. Rebase leaves a clear marker:

bash
git reflog
text
7d4c1a2 (HEAD -> feature) HEAD@{0}: rebase (finish): returning to refs/heads/feature
7d4c1a2 HEAD@{1}: rebase (squash): combine auth commits
b8e3f01 HEAD@{2}: rebase (start): checkout main
2a9c6d4 HEAD@{3}: commit: add password reset endpoint

The rebase (start) line is the boundary. The commit just before it, HEAD@{3} here (2a9c6d4), is your branch exactly as it stood before the rebase began. To throw away the rebase result and restore the original branch:

bash
git reset --hard HEAD@{3}

Substitute the hash you actually see; the HEAD@{n} numbers shift every time HEAD moves, so always read your own reflog rather than copying mine. If you want to learn the operation itself rather than recover from it, squashing commits with interactive rebase and git merge vs git rebase cover the deliberate use.

Recovering a deleted branch

Delete a branch and its label is gone, but the commit it pointed at is still in the reflog. Say you deleted feature/checkout:

bash
git branch -D feature/checkout
text
Deleted branch feature/checkout (was 5f2a8c1).

Git even tells you the hash right there in the message (5f2a8c1). If you missed it, find the branch's last position in the reflog:

bash
git reflog

Look for the line where you last had that branch checked out or committed to it, then recreate the branch at that commit:

bash
git branch feature/checkout 5f2a8c1

The branch is back, pointing at the same commit, with all its history intact. You can also recreate and switch to it in one step with git checkout -b feature/checkout 5f2a8c1.

reset, checkout, or branch: which recovery command?

Once you have the hash, you have three ways to bring the commit back. Pick based on what you want to happen to your current branch.

CommandWhat it doesUse it when
git reset --hard <hash>Moves your current branch tip to that commit and matches the working tree to itYou want the current branch to become the recovered state and you have no uncommitted work to lose
git checkout <hash>Puts you on the commit in detached HEAD, leaving every branch untouchedYou want to inspect or copy the commit first, without committing to a branch move
git branch <name> <hash>Creates a new branch pointing at the commitYou are recovering a deleted branch, or want the old state on its own branch without disturbing the current one

When you are unsure, git checkout <hash> is the gentlest move: it changes no branch, lets you look around and confirm this is the commit you wanted, and you can then create a branch from there. The deeper distinction between resetting and reverting is in git reset vs git revert, and the everyday undo cases live in how to undo the last Git commit.

Recovering a single commit's changes onto your current branch

Sometimes you do not want to move your whole branch back, you just want one lost commit's changes applied on top of where you are now. Cherry-pick it by hash:

bash
git cherry-pick 9b7d2a4

That replays just that commit as a new commit on your current branch, leaving everything else in place. It is the right tool when a reset or rebase scrambled the order and you only need one piece back.

When the reflog cannot help you

The reflog is excellent, but it is not magic. It will not save you in a few specific situations:

  • Uncommitted work. The reflog tracks commits, not your working tree. If you git reset --hard over edits you never committed (or never staged), there is nothing for the reflog to point at. This is the real reason to commit often, even with throwaway messages you clean up later. See Git commit message best practices and the role of the staging area.
  • A fresh clone or a different machine. The reflog is local to one clone. A commit that only ever existed in a clone you deleted, and was never pushed, is genuinely gone.
  • After garbage collection past the grace period. Run git gc --prune=now (or wait out the 30-day default for unreachable reflog entries) and unreachable commits really are pruned. This is rare to hit by accident, though it is exactly the mechanism you reach for deliberately when purging a secret from history.

In those cases, the answer is usually the remote: if you ever pushed the work, git fetch and the remote tracking branches have it. This is also the path back when a force-push went wrong, which often goes hand in hand with recovering from a non-fast-forward push rejection. Understanding the difference between fetching and pulling helps here (git pull vs git fetch).

A quick recovery checklist

  1. Do not panic and do not run more destructive commands. Each new reset --hard or rebase pushes the good state further down the reflog, but it is still there.
  2. Run git reflog and read it top to bottom, newest first.
  3. Find the line describing the state you want back (the commit, checkout, or rebase (start) just before the mistake).
  4. Decide: become that state (reset --hard), inspect it first (checkout), or save it on a branch (branch <name>).
  5. Verify with git log and git status that you are where you expect to be.

The best recovery is the one you never need; a fair amount of this comes down to how Git workflows keep teams out of this mess by making destructive resets rare in the first place.

Sources

Authoritative references this article was fact-checked against.

Tagsgit reflogGitVersion Controlrecover commitsgit resetundo

Found this useful? Pass it on.

Copied

Ishan Karunaratne

Tech Architect · Software Engineer · AI/DevOps

Tech architect and software engineer with 20+ years building software, Linux systems, and DevOps infrastructure, and lately working AI into the stack. Currently Chief Technology Officer at a healthcare tech startup, which is where most of these field notes come from.

Keep reading

Related posts

How to Resolve Merge Conflicts in Git

A merge conflict means Git found two changes to the same lines and needs you to pick. Here is how to read the conflict markers, fix them by hand or with a mergetool, and commit.

How to Remove a Secret from Git History

Committed an API key or password? Deleting it in a new commit does not remove it. Rotate the credential first, then scrub it from all history with git filter-repo or BFG.