You've just run git reset --hard HEAD~5. The terminal goes quiet. Your stomach drops.
Five commits — three hours of work — are gone. You didn't push them. You didn't open a PR. Your editor doesn't have them in its undo buffer anymore. The Git object database has freed the references and is going to garbage-collect them eventually.
You can get every one of them back, perfectly, in about forty seconds.
This is the most important command in Git that almost nobody learns until they've already lost work twice. It's called git reflog.
What reflog actually is
A "ref" in Git is just a pointer. HEAD is a ref. Every branch name is a ref. Every tag is a ref. When you make a commit, Git updates whatever ref you're currently sitting on to point at the new commit's SHA. When you switch branches, the HEAD ref moves to the tip of the new branch.
The reflog is a journal of every ref movement. Every time HEAD shifts — every commit, every checkout, every merge, every reset, every rebase step — Git writes a line into the reflog with the previous SHA, the new SHA, and a short reason like "commit" or "reset: moving to HEAD~5".
That journal lives in .git/logs/HEAD and .git/logs/refs/heads/<branch>. It's local to your machine. It's not pushed anywhere. It's not part of the repo. Nobody else can see it.
And, critically — and this is the part that saves your three hours of work — the commits the reflog references are not deleted until garbage collection runs, which by default is 30 days from now. Even when those commits are no longer reachable from any branch, no longer in any working tree, no longer mentioned by any tag — they sit in .git/objects/, untouched, waiting for you to point a branch at them.
A failed reset --hard doesn't delete commits. It just unparks them.
The commands you actually need
There are only three. Memorise them.
git reflog
git reflog show <branch>
git reset --hard <sha-or-ref>
Let's walk through a recovery from start to finish.
Step 1: Look at the reflog
git reflog
You'll get something like:
a1b2c3d HEAD@{0}: reset: moving to HEAD~5
9f4e2c8 HEAD@{1}: commit: feat: wire payment confirmation email
6c1a5e3 HEAD@{2}: commit: feat: add payment confirmation route
3d8f2b1 HEAD@{3}: commit: chore: bump stripe SDK
0e7a9d4 HEAD@{4}: commit: refactor: extract email queue
b4c8e2f HEAD@{5}: commit: feat: add email queue worker
2a1b3c4 HEAD@{6}: checkout: moving from main to feature/payments
Read this top-down as "what HEAD just did, in reverse order". The most recent thing is HEAD@{0} — the reset --hard we just regretted. The five commits we lost are right there at HEAD@{1} through HEAD@{5}.
Step 2: Move HEAD back
The simplest recovery: put your branch back where it was before the reset. The reflog tells you the SHA of the commit you regret leaving — it's the entry that says commit: immediately above the bad reset: line. In the example above, that's 9f4e2c8.
git reset --hard 9f4e2c8
That's it. Your branch is now at 9f4e2c8 again. All five commits are reachable from the tip. git log shows them. Your working tree matches commit 9f4e2c8. Three hours of work, restored.
The four scenarios where reflog saves you
1. Hard reset you regret
The scenario above. Either you reset more than you meant to, or you reset and then realised "wait, I needed that branch state for something."
git reflog # find the SHA you want back
git reset --hard <sha> # move HEAD there
2. Deleted a branch with unmerged work
git branch -D feature/payments
# (panic)
Branches are just refs. Deleting one removes the pointer but leaves the commits intact for the reflog window. Recovery:
git reflog | grep payments # find the last SHA the branch was at
git checkout -b feature/payments <sha>
3. Rebase ate someone else's commits
You ran git rebase main and resolved a conflict by picking your side. Now you realise that side dropped Alice's commits that were on the branch before you started.
git reflog
Find the entry that says rebase: start or rebase (start). Whatever HEAD was at just before the rebase began is your pre-rebase state. Reset to it.
4. Force-pushed over a teammate's work
Your teammate pushed something. You force-pushed your version over it without pulling first. Their commits are gone from the remote — and from your local, because you git push --force'd.
# On their machine:
git reflog | head -20 # they can still see their commits
git push --force origin feature # re-publish their state
For this exact reason: never git push --force. Use git push --force-with-lease.
The thing reflog won't save you from
Reflog tracks ref movement. It does not track changes to files you never committed.
If you ran git checkout -- <file> (or its modern equivalent git restore <file>) on a file you'd been editing but never staged, that work is lost. The file's pre-edit state replaced your edits in the working tree, and nothing was ever written to the object database. The reflog can't help.
The rule: anything Git ever assigned a SHA to is recoverable for ~30 days. Anything Git never saw is gone the moment you tell it to discard.
How long do I actually have?
By default, two windows:
-
Reachable but unreferenced commits (e.g., dangling after a reset): 90 days before
git gcis allowed to remove them. - Unreachable commits (orphan branches, dropped stashes): 30 days.
You can also extend these in ~/.gitconfig if you want longer recovery windows:
[gc]
reflogExpire = 365.days
reflogExpireUnreachable = 365.days
I run with 365 on personal machines. The disk cost is negligible, and the peace of mind is real.
A practical habit
I keep this command aliased in my .gitconfig:
[alias]
rl = reflog --date=relative
oops = !git reset --hard HEAD@{1}
The first gives me a human-friendly reflog with relative dates. The second is the literal undo button: it moves HEAD back to where it was one operation ago, no matter what that operation was. git oops is the muscle memory I want when I realise I just did something wrong.
Try this once today: do a git reset --hard HEAD~1 on a throwaway branch, then recover with git oops. The first time you do it deliberately, the panic-flavored memory of doing it accidentally evaporates.
You don't need to learn Git's internals to ship code. But you do need to know the reflog exists. The next three-hour mistake you save yourself from will make it the highest-ROI five minutes of Git education you ever have.
I wrote this as part of gitflow.dev — interactive Git training with a real Git engine in the browser. Free, no sign-up to read. If you found this useful, the reflog recovery scenario lets you try the recovery in a live terminal.
Top comments (0)