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.

DevelopmentGitVersion Control

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 --hard overwrote isn’t in it.
  • Other repositories: Reflogs are local. main@{yesterday} means where your local main was yesterday.
  • Bare repositories: core.logAllRefUpdates defaults to true with a working directory and false in 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.

Back to the blog