Guide10 min read

Recover Lost Commits With git reflog

Git almost never deletes a commit when you think it does. The reflog records everywhere HEAD has been, which means most of the moments that feel unrecoverable take about thirty seconds to fix.

Recover Lost Commits With git reflog | Gitoryx

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:

LabelWritten by
commitA normal commit
commit (amend)git commit --amend
commit (initial)The first commit in the repository
checkoutSwitching branches, including into detached HEAD
resetAny 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
pullA 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.

Frequently Asked Questions

How long do reflog entries last?

Entries for reachable commits expire after 90 days by default, and entries for unreachable ones after 30 days. Both are controlled by `gc.reflogExpire` and `gc.reflogExpireUnreachable`. In practice you have three months to recover from a mistake, which is far longer than the window in which anyone notices.

Can I use the reflog to recover a commit a teammate lost?

No. The reflog is strictly local and is never pushed or fetched. If a colleague force-pushed over commits you had, your own reflog may still hold them if you had fetched them, but you cannot read theirs. Recovery from someone else's reflog means asking them to run the commands on their machine.

Does git reflog show commits I never committed?

No. The reflog only records movements of HEAD and branch tips, which means committed work. Changes that were only staged or only in the working tree when you ran `git reset --hard` are gone. Staged changes are sometimes recoverable from dangling blobs with `git fsck --lost-found`, but uncommitted, unstaged work is not.

What is the difference between git reflog and git log?

`git log` walks the ancestry of a commit: what leads to where you are now. `git reflog` walks time: everywhere HEAD has pointed, in the order it happened, including branches that no longer exist and commits that nothing references anymore.

All Blog Posts