The Reflog and Recovering Anything

Why almost nothing committed is ever really lost, how to read the reflog under pressure, and the two situations where recovery genuinely is impossible.

advanced 20 min lesson hands-on task included

The reflog is the reason experienced Git users stay calm. Commits are immutable objects; destructive commands move refs. The reflog records every place a ref has been, so almost every disaster is a matter of pointing a ref back where it was.


Topic 1: What the Reflog Records

THE REFLOG RECORDS EVERY MOVE OF HEAD — INCLUDING THE ONES THAT "DELETED" WORK $ git reflog 9f8e7d HEAD@{0} reset: moving to HEAD~3 3c4d5e HEAD@{1} commit: add retry logic 7a8b9c HEAD@{2} commit: extract client 1f2e3d HEAD@{3} checkout: moving from main to feature ← the work you thought was gone is right here 1. THE MISTAKE git reset --hard HEAD~3 Three commits unreferenced. 2. FIND IT git reflog Read the message column. 3. GET IT BACK git reset --hard 3c4d5e or: git branch rescue 3c4d5e WHAT THE REFLOG CAN RECOVER · a hard reset · a deleted branch · a bad rebase · an amended commit · a botched merge · commits made on a detached HEAD git reflog · git reflog show BRANCH · ORIG_HEAD WHAT IT CANNOT · changes never committed (nothing recorded them) · entries older than 90 days (30 for unreachable) · anything after an explicit gc --prune=now Commit early. The reflog only knows what git knew.
The middle column is the whole trick: read the message column to find the operation that lost the work, then point a ref at the hash on that line. The two bottom panels are the boundary — what it can and cannot recover.

Every time a ref moves — commit, checkout, reset, merge, rebase, pull, amend — Git appends a line to that ref’s log in .git/logs/.

git reflog                       # HEAD's movements
git reflog show main             # one branch's movements
git reflog --date=iso            # with real timestamps
9f8e7d HEAD@{0}: reset: moving to HEAD~3
3c4d5e HEAD@{1}: commit: add retry logic
7a8b9c HEAD@{2}: commit: extract client
1f2e3d HEAD@{3}: checkout: moving from main to feature/retry

Read the message column first. It names the operation, so you can find the moment things went wrong without knowing any hashes. HEAD@{1} is where HEAD was one move ago.

The reflog is local and personal. It is not pushed, not cloned, and not shared. A colleague’s reflog cannot help you, and yours cannot help them — but every clone has its own, which is occasionally the thing that saves a force-pushed branch.


Topic 2: The Four Common Recoveries

A hard reset took real work:

git reflog                     # find the commit before the reset
git reset --hard 3c4d5e        # or, safer:
git branch rescue 3c4d5e       # inspect first, decide after

Creating a branch rather than resetting is the better reflex under pressure — it is non-destructive, and you can look before committing to anything.

A deleted branch:

git reflog | grep -i "checkout: moving from feature/retry"
git branch feature/retry 3c4d5e

Git also prints the tip hash when you delete a branch (Deleted branch feature/retry (was 3c4d5e)). If that line is still in your terminal scrollback, it is the fastest recovery available.

An amend that overwrote a good commit:

git reflog                     # the pre-amend commit is one entry back
git reset --hard HEAD@{1}

Commits made on a detached HEAD, then abandoned:

git reflog                     # they are there, under "commit"
git branch rescue-work <sha>

Topic 3: When the Reflog Cannot Help — fsck

If a ref never pointed at the commit (say, a lost stash, or an object from an interrupted operation), the reflog has no entry. The object may still exist:

git fsck --lost-found
# dangling commit 3c4d5e...
# dangling blob 8a9b0c...

git show 3c4d5e                # is this the one?
git branch rescue 3c4d5e

--lost-found writes dangling commits and blobs into .git/lost-found/, which is the last resort when you know content exists but nothing references it.

Recovering a dropped stash, the classic use:

git fsck --unreachable | grep commit | cut -d' ' -f3 |
  xargs git log --merges --no-walk --format='%H %ci %s'
# stash commits have two or three parents and a "WIP on ..." message
git stash apply <sha>

Finding a blob whose commit is gone — when you know a file’s content existed:

git fsck --lost-found
grep -l "the string you remember" .git/lost-found/other/*

Ugly, and it works.


Topic 4: The Two Things Git Cannot Recover

1. Work that was never committed. No object was ever written, so there is nothing to find. This covers:

  • Edits discarded by git restore / git checkout -- .
  • Files removed by git clean
  • Uncommitted changes destroyed by git reset --hard
  • Anything lost while the file was untracked

Your editor’s local history or your filesystem snapshots are the only recourse, and neither is Git.

2. Objects that have been garbage collected. Unreachable objects survive for a grace period — 90 days for reflog entries, 14 days for unreachable objects — then gc removes them. In normal use that window is generous. It closes immediately if somebody runs:

git reflog expire --expire=now --all && git gc --prune=now --aggressive

That command sequence is the one true point of no return in everyday Git, and it appears in a lot of “clean up your repository” advice with no warning attached. Run it only when you specifically intend to make unreachable objects unrecoverable — for example, after a history rewrite that removed a secret.


Topic 5: The Cheap Insurance

Habits that cost seconds and remove most of the risk:

# Before anything destructive, take a free escape hatch
git tag backup/$(date +%Y%m%d-%H%M%S)
git branch backup/before-rebase

A tag or branch costs 41 bytes and turns any rewrite into something you can walk back from with one command. Delete it a week later.

Commit more often than feels necessary. A messy commit is recoverable; an uncommitted change is not. You can always clean up the history later with an interactive rebase — that is precisely what it is for.

Push work-in-progress branches. A branch on the forge survives a laptop failure, a disk error, and a rm -rf in the wrong directory. Nothing local does.

Extend the reflog window in repositories that matter:

git config gc.reflogExpire "365 days"
git config gc.reflogExpireUnreachable "90 days"

Topic 6: Recovering a Force-Pushed Branch

Somebody force-pushed over main and a day of everyone’s commits vanished from the remote. This is the incident that feels worst and is usually straightforward, because the objects still exist in every clone that fetched them.

# 1. Do NOT let anyone pull or prune — that is what actually destroys the copies.
#    Announce this first, before any commands.

# 2. On any clone that has the old commits, find them:
git reflog show origin/main
git log --oneline origin/main@{1}

# 3. Verify it is the right tip
git log --oneline <old-sha> -20

# 4. Push it back
git push origin <old-sha>:main --force-with-lease

If nobody’s reflog has it, the forge probably does: GitHub’s activity/events view and GitLab’s ref logs record pushed hashes, and a hash is all you need to fetch the objects back while they remain unpacked on the server.

Then fix the cause, not just the symptom: enable branch protection so main cannot be force-pushed at all. This is a two-minute forge setting and it is the entire prevention.

Try it yourself: in a scratch repository, git reset --hard HEAD~5, then recover with the reflog and time it. Doing this once, calmly, is what makes the real version — at 6pm, with an audience — a two-minute event instead of a crisis.

Common mistake: panicking and running more commands. A git pull, a git gc, or a git reset on top of the original mistake can turn a trivially recoverable situation into a genuinely difficult one. The correct first move is always git reflog and git status — both read-only — followed by a backup branch before anything else.