← back to latest

Bring back the commits you lost to a bad reset

git reflog show --date=relative HEAD

anatomy

git reflog
Read the reference log, a per-ref record of every value that ref has held. It is not part of your history and not part of the commit graph. It is a plain append-only text file under .git/logs/, one line per move, holding the old SHA, the new SHA, who did it, when, and what kind of operation it was. Because git writes that line before it does anything else, the commit you cannot see is still named somewhere on disk.
show
The subcommand that reads. It is the default, so bare `git reflog` does the same thing, but the other two subcommands are `expire` and `delete`, and both destroy entries. Typing `show` costs four characters and removes the chance of reaching for the wrong one under pressure.
--date=relative
Render each entry as its age. This is how you find the right line: you rarely remember a SHA, but you do remember that the rebase went wrong about an hour ago. Note that this replaces the index in the selector rather than adding to it. See the second gotcha.
HEAD
Which ref's log to read. HEAD is the default and the one you want after a reset, because it records every position you have been in regardless of branch. Every branch also keeps its own log, so `git reflog show invoice-tax` answers a narrower question: what has this one branch pointed at.

Sample output

4987c0b HEAD@{3 hours ago}: reset: moving to HEAD~3
c101e66 HEAD@{3 hours ago}: commit: Add tax rate fixtures
fe7a042 HEAD@{3 hours ago}: commit: Handle zero-rated goods
bf49093 HEAD@{4 hours ago}: commit: Add VAT calculation
4987c0b HEAD@{4 hours ago}: checkout: moving from main to invoice-tax
4987c0b HEAD@{5 hours ago}: commit: Add invoice model
5679df4 HEAD@{5 hours ago}: commit (initial): Initial commit

Read the top line as the damage and the second line as the fix. The reset moved HEAD to 4987c0b, and the entry directly beneath it is where HEAD stood immediately before: c101e66, the tip of the three commits that stopped being reachable. That SHA is the whole answer. Everything after this is deciding what name to give it.

When you would reach for it

A git reset --hard with the wrong argument, a rebase that collapsed the wrong commits, a branch you deleted while convinced it was merged, or the specific dread of git checkout . over an afternoon of work. It also answers the calmer question of what you were doing yesterday, since every checkout, merge, commit, and rebase step leaves a line. If you have ever tried to reconstruct which branch you were on when something broke, the reflog already wrote it down.

Gotchas

  • HEAD@{1} and HEAD~1 look alike and mean nothing like each other. The tilde walks the commit graph: HEAD~1 is the parent, 5679df4 Initial commit in the tree above. The braces index the reflog: HEAD@{1} is wherever HEAD stood one move ago, c101e66 Add tax rate fixtures, a commit that is not an ancestor of HEAD at all. After a bad reset the parent is the last thing you want, so reaching for the wrong one moves you further from the work rather than back to it.
  • --date=relative swaps the index out of the selector rather than printing both, so you get HEAD@{3 hours ago} and lose HEAD@{0}. This is not a rendering quirk you can format around: --date rewrites %gd and %gD too, so a custom format cannot recover the index while a date is in play. The timestamps it prints are usable as selectors, which softens it, though see the next bullet.
  • A date-based selector that falls outside the log does not fail. Ask for HEAD@{6.months.ago} in a repository cloned last week and git prints a warning that the log only goes back so far, then hands you the oldest entry anyway. The warning goes to stderr and the wrong commit goes to stdout, so in a pipeline or a script the fallback is silent. Check the SHA you got against the SHA you meant before acting on it.
  • The reflog is local, and nothing ships it. Clone a repository and its reflog starts fresh with one line, clone: from ..., no matter how much history came with it, and bare repositories have core.logAllRefUpdates off by default, so the server has no reflog to consult either. Entries also expire: the documented defaults prune reachable entries after 90 days and unreachable ones after 30. The unreachable number is the one that matters, because unreachable is exactly what your lost commit is.

Variants

$ git branch tax-recovery 'HEAD@{1}'

Recover by naming the commit instead of moving to it. This creates a branch and touches nothing else, so your working tree, current branch, and the reflog itself are unchanged, which makes it safe to run before you are sure it is the right SHA. Quote the selector so the shell cannot treat the braces as expansion

$ git reflog show invoice-tax

Read one branch’s log rather than HEAD’s. Narrower and easier to scan, since it excludes every checkout that merely passed through. Deleting a branch deletes this log with it, so after a git branch -D the HEAD reflog is the copy that survives

$ git reflog --format='%gd %h %gs (%cr)'

Keep the HEAD@{0} index and add an age. Read the parenthesized time carefully: %cr is the commit’s own date, not the date of the reflog entry, so a reset that replays an old commit shows that commit’s age rather than the moment you ran the reset

lineage

The reflog exists because Git's own model made a certain kind of accident routine. Commits are immutable and content addressed, but branches are nothing more than a file holding a SHA, so any command that moves a branch quietly abandons whatever it used to point at. The object survives in the database, unreferenced and invisible, until garbage collection eventually sweeps it. The fix, added in the mid-2000s as Git spread beyond the kernel developers who first used it, was not to change that model but to journal it: before any ref moves, append a line recording where it was. That is why the reflog is a flat text file rather than anything cleverer, and why it lives outside the commit graph instead of inside it. It is deliberately not history. It is a log of the times you changed your mind, which is a different thing and, on a bad afternoon, a more useful one.