Getting commits back after git reset --hard with reflog
The reflog records where branches and HEAD pointed in your local repo. Find a lost commit, restore it as a branch, and learn the default 90/30-day expiry and when recovery fails.
You run git reset --hard HEAD~2, then realize you needed those two commits. They’re gone from git log, but usually still in the repository, and the reflog shows where.
What the reflog records
The Git manual says reflogs record “when the tips of branches and other references were updated in the local repository.” Commits, resets, rebases, and checkouts add an entry when they move a ref, and the HEAD reflog also records branch switches.
git reflog is the same as git reflog show and defaults to HEAD. HEAD@{2} is where HEAD was two moves ago, and time forms like main@{yesterday} work too.
Recovering, step by step
git reflog # find the entry just before the reset
git branch rescue HEAD@{1} # create a branch at that position
git branch <name> <start-point> creates a branch at a commit ID or reflog expression without touching your working tree, so you can inspect it before merging or resetting to it. Right after the reset, git branch rescue ORIG_HEAD also works, because git reset saves the branch tip to ORIG_HEAD before moving it.
How long entries last
Reflog entries expire. When git gc runs git reflog expire, the defaults are:
- Reachable from the current tip of the ref whose reflog is being expired:
gc.reflogExpire, 90 days - Not reachable from that tip (e.g., commits dropped by reset or amend):
gc.reflogExpireUnreachable, 30 days
git gc keeps objects referenced by reflogs. Once the relevant entry expires and no other ref reaches an object, it may be pruned in the same run if it’s already older than the gc.pruneExpire cutoff (two weeks by default). The two weeks is object age, not extra time after the reflog expires. Settings or manual expire/prune commands can shorten this, so recover right away.
What the reflog can’t do
- Uncommitted changes: The reflog records ref movements only, so uncommitted work that
reset --hardoverwrote isn’t in it. - Other repositories: Reflogs are local.
main@{yesterday}means where your localmainwas yesterday. - Bare repositories:
core.logAllRefUpdatesdefaults totruewith a working directory andfalsein bare repositories, so bare repositories may have no reflogs by default.
Defaults are configurable, so check yours with commands like git config --get gc.reflogExpire.