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}andHEAD~1look alike and mean nothing like each other. The tilde walks the commit graph:HEAD~1is the parent,5679df4 Initial commitin 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=relativeswaps the index out of the selector rather than printing both, so you getHEAD@{3 hours ago}and loseHEAD@{0}. This is not a rendering quirk you can format around:--daterewrites%gdand%gDtoo, 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 havecore.logAllRefUpdatesoff 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