You meant to run git reset --hard HEAD~1. You ran git reset --hard HEAD~10. The
terminal prints one line, your branch is nine commits shorter than it should be, and
there is a specific feeling that goes with that.
The work is not gone. Git did not delete anything. It moved a pointer, and it wrote down where the pointer used to be.
That record is the reflog, and it is the reason very few Git mistakes are actually permanent.
#What the reflog is
Every time HEAD moves, Git appends a line to a log. Commits move HEAD. So do checkouts, merges, rebases, resets, and pulls. Each line records the commit HEAD moved to, the operation that caused it, and a sequence number.
This is a fundamentally different structure from your commit history. git log answers
"what is the ancestry of my current commit". The reflog answers "where has this
repository been, in chronological order". A commit that has been reset away, or that sat
on a branch you deleted, disappears from the first and stays in the second.
Three properties matter in practice:
It is local. The reflog lives in .git/logs/ and is never pushed or fetched. Your
reflog is yours. A clone starts with an empty one.
It expires. Reachable entries are pruned after 90 days, unreachable ones after 30. You are not carrying this forever, but you have far longer than the window in which anyone notices a mistake.
It only knows about commits. If your work was staged or merely saved when you ran
reset --hard, the reflog cannot help. Committing early is what makes the safety net
work, which is a good argument for treating commits as checkpoints rather than
milestones.
#Reading it
git reflog
b7c2e91 (HEAD -> feature/billing) HEAD@{0}: reset: moving to HEAD~10
4a91f3d HEAD@{1}: commit: ✨ add invoice PDF export
9e02c14 HEAD@{2}: commit: ✅ cover the proration edge cases
1f7ab60 HEAD@{3}: rebase (finish): returning to refs/heads/feature/billing
1f7ab60 HEAD@{4}: rebase (pick): ♻️ extract the proration calculator
d3c8a55 HEAD@{5}: checkout: moving from main to feature/billing
Two things to read. HEAD@{N} is a positional reference: HEAD@{0} is where you are
now, HEAD@{1} is where you were one move ago. The text after the colon is the operation
that caused the move, which is how you find your place without recognising hashes.
Here, HEAD@{1} is the commit you were on immediately before the reset. That is the one
you want.
You can also filter by branch, which is quieter than the global log:
git reflog show feature/billing
And you can address entries by time rather than position, which is easier when the log is long:
git reflog --date=iso
git show 'HEAD@{2.hours.ago}'
git show 'main@{yesterday}'
The operation labels are the part worth learning, because they are how you find your place without recognising hashes. The ones you will actually search for:
| Label | Written by |
|---|---|
commit | A normal commit |
commit (amend) | git commit --amend |
commit (initial) | The first commit in the repository |
checkout | Switching branches, including into detached HEAD |
reset | Any git reset |
merge <branch> | A merge that produced a commit |
rebase (start) | The moment a rebase began |
rebase (pick) | Each commit replayed during a rebase |
rebase (finish) | The rebase completed |
pull | A fetch plus merge |
rebase (start) is the single most useful one. The entry directly below it in the output
is your branch exactly as it was before the rebase touched anything.
git reflog | grep -n "rebase (start)"
#Scenario: undoing a bad reset
The one from the opening. You reset too far and want your commits back.
# 1. Find where you were.
git reflog
# 2. Confirm the entry is the one you think it is.
git show HEAD@{1}
# 3. Move the branch back to it.
git reset --hard HEAD@{1}
That is it. The branch tip returns to the commit it pointed at before the reset, and the nine commits reappear.
If you are not certain, do it non-destructively first. Create a branch at the recovered point, look at it, and only then decide:
git branch recovered HEAD@{1}
git log recovered
Nothing about your current state changes, and you can delete recovered if it turns out
to be the wrong entry. This is the version to use when you are stressed, because it has
no failure mode.
#Scenario: recovering a deleted branch
You ran git branch -D feature/invoices and then realised it had not been merged.
Deleting a branch deletes a pointer. The commits are untouched, just unreferenced. You need the hash of the tip.
git reflog | grep invoices
4a91f3d HEAD@{14}: checkout: moving from feature/invoices to main
That line records the moment you left the branch, so 4a91f3d is the tip as it was then.
Recreate the branch there:
git branch feature/invoices 4a91f3d
If the grep finds nothing, usually because you never checked the branch out from this
clone, git fsck will list commits that nothing points at:
git fsck --lost-found --no-reflogs
That is noisier and includes genuine garbage, but the commit is in there.
#Scenario: a rebase that went wrong
Rebases produce a lot of reflog entries, one per replayed commit, which makes the log look intimidating. There is a shortcut.
Before starting a rebase (or a merge, or a hard reset), Git writes the previous HEAD to
ORIG_HEAD. So immediately after a rebase you regret:
git reset --hard ORIG_HEAD
That is the whole recovery, provided you have not run another history-moving operation
since, because ORIG_HEAD is overwritten each time.
If you have moved on, go back to the reflog and look for the rebase (start) entry. The
line immediately before it is your pre-rebase state:
git reflog | grep -n "rebase"
git reset --hard HEAD@{9}
The same applies to a git pull that turned into an unexpected merge, which is one of
the more common ways people end up with a history they did not intend. If you are unsure
what a reset flag will do before you run it, the
git reset tutorial has the comparison.
#Scenario: recovering a dropped stash
A stash is a commit, and refs/stash has its own reflog. So git stash drop, or a pop
that hit a conflict and left you unsure what happened, is recoverable the same way.
git reflog stash
stash@{0}: WIP on main: 4a91f3d ✨ add invoice PDF export
stash@{1}: On feature/billing: proration experiment
If the entry is still listed, apply it by name. If you dropped it and the ref is gone, the commit is unreferenced but still in the object database:
git fsck --no-reflog | awk '/dangling commit/ {print $3}'
That prints candidates. Inspect them until you find the right one, then apply it:
git show <sha>
git stash apply <sha>
Most people give up on a dropped stash because it feels final. It is not, for the same 30 day window as anything else unreachable.
#Scenario: an amend you regret
git commit --amend replaces the tip commit with a new one. The original is unreferenced
immediately, and if the amend discarded changes rather than adding them, that content is
only in the reflog.
git reflog
7e2d508 HEAD@{0}: commit (amend): ✨ add invoice PDF export
b7c2e91 HEAD@{1}: commit: ✨ add invoice PDF export
HEAD@{1} is the pre-amend commit. To get its content back without moving your branch:
git checkout HEAD@{1} -- .
Or to discard the amend entirely:
git reset --hard HEAD@{1}
The commit (amend) label in the reflog is what makes this findable. Every operation
writes its own label, and learning to recognise four of them, commit, commit (amend),
rebase (start) and reset, covers most of what you will ever need to search for.
#What the reflog cannot do
Being honest about the limits matters, because "the reflog will save you" is repeated often enough that people rely on it in situations where it does not apply.
It cannot recover uncommitted work. git reset --hard with unstaged changes destroys
them and nothing records what they were. Staged changes leave dangling blobs that
git fsck --lost-found can sometimes surface, but reconstructing a file from loose blobs
is unpleasant. Commit before you experiment.
It cannot reach into someone else's repository. If a colleague force-pushed over commits you never fetched, your reflog has no record of them. Theirs does. Recovery means asking them to run these commands.
It does not survive a fresh clone. A clone starts with an empty reflog. If the only copy of a commit was in the reflog of a machine you have wiped, it is gone.
It expires. Ninety days for reachable entries, thirty for unreachable. Also, an
aggressive git gc --prune=now can clear unreachable objects earlier, so avoid running
that while you are still figuring out whether something is lost.
#Where it lives, and how to keep it longer
The reflog is a plain text file per ref, which is worth knowing because it means you can read it without Git and back it up by copying a directory.
cat .git/logs/HEAD
cat .git/logs/refs/heads/main
Each line is: old SHA, new SHA, author, timestamp, then the operation label. That is the entire format.
Expiry is controlled by two settings, and on a repository where you would rather not lose anything, extending them costs a few kilobytes:
# Never expire reachable entries.
git config gc.reflogExpire never
# Keep unreachable ones for a year instead of 30 days.
git config gc.reflogExpireUnreachable "1 year"
Set these per repository rather than globally. On a machine with a lot of clones the storage adds up, and on most repositories the defaults are already generous.
One thing not to do while you are recovering something: git gc --prune=now, or anything
that runs it, such as some aggressive repository cleanup scripts. It removes unreachable
objects immediately and it is the only realistic way to lose something the reflog was
about to give you back.
#Making it readable
The reflog is a flat text log, and the operation that matters to you might be at position fourteen. You are matching hashes against your memory of what you were doing, which is exactly the kind of task humans are bad at when they are already stressed.
Gitoryx has a reflog viewer that lists the entries with each one's commit shown in context: the subject line, the author, the diff, and where that commit sits in the graph. Recognising the right entry becomes visual rather than a matter of recalling hashes, and checking out, branching from, or resetting to an entry is one action from the row you picked.
Resetting from a reflog viewer is still a reset, in any client. Confirm your working tree is clean before you use it, because the safety net that recovers commits does nothing for uncommitted changes.
The commands are the same ones above. What changes is that you are choosing a commit you can see rather than an index into a list.
#The habit worth forming
Reach for the reflog earlier than feels natural.
The instinct after a destructive command is to try to reconstruct the lost state by hand,
or to conclude the work is gone and start again. Both are usually wrong and both take
longer than git reflog. The commit is almost certainly still there, and the entry you
need is almost certainly in the first ten lines.
And when the mistake is smaller than a lost branch, the reflog is not always the right tool. A commit on the wrong branch, a message you want to change, a file staged by accident: each has a more direct answer, and our undo a commit tutorial covers the ones you will hit most often.
