git · level 7

Reflog & Recovery

git reflog, git fsck — why no commit truly disappears for 30 days.

175 XP

Reflog & Recovery

The single most reassuring thing about git: nothing is really lost for 30+ days. Commits, branches, even bad rebases — almost everything is recoverable from the reflog. This lesson is the mental model that lets you take risks fearlessly.

Analogy

The reflog is your CCTV footage. You may have walked out of the room (deleted the branch, reset HEAD), but the camera was rolling the whole time and the tape is in the basement. Nobody filed any of it; nothing is labelled — but if you can give security an approximate time, they can replay it. After about three months the tapes get overwritten (garbage collection), so don't dawdle when something goes wrong, but don't panic either. The footage exists.

The reflog — every move HEAD made

git reflog is a private, local-only log of every position HEAD has been at in this clone. Every commit, every checkout, every reset, every rebase step, every merge, every amend — all logged with a timestamp.

$ git reflog
abc1234 HEAD@{0}: reset: moving to HEAD~3
def5678 HEAD@{1}: commit: add user tests
9876fed HEAD@{2}: pull: Fast-forward
fedcba9 HEAD@{3}: checkout: moving from main to feat/x
abc1234 HEAD@{4}: commit: WIP search
...

Reading it:

  • HEAD@{N} — N positions ago. HEAD@{0} is the current state, HEAD@{1} is one move ago, etc.
  • The SHA on the left is what HEAD pointed at after that move.
  • The action is what caused the move (commit, reset, checkout, etc).

You can address any past state by its reflog name OR by the SHA. Both work in any git command:

git diff HEAD@{1}                # diff against where I was one move ago
git reset --hard HEAD@{2}        # rewind to where I was two moves ago
git reset --hard abc1234         # same thing, by SHA

The four classic recovery scenarios

1. Lost commits from reset --hard

You ran git reset --hard HEAD~3 and your last three commits are gone. They're not gone — they're orphaned but still in the object database.

git reflog
# 5e6f7a8 HEAD@{0}: reset: moving to HEAD~3
# d4e5f6a HEAD@{1}: commit: third commit you thought you lost
# c3d4e5f HEAD@{2}: commit: second one
# b2c3d4e HEAD@{3}: commit: first one

git reset --hard HEAD@{1}        # back to before the bad reset

You're whole again.

2. Deleted local branch

You ran git branch -D feat/x. Branch is gone but the commits are still there. Reflog tracks per-branch history too — even after the branch is deleted, its reflog can be queried for a window:

git reflog                       # find the tip SHA of the deleted branch
# look for the last commit on feat/x
# alternatively:
git fsck --lost-found            # finds dangling commits
git branch feat/x <sha>          # recreate

3. Bad rebase

You rebased feat/x onto a stale base; the result is a mess. The pre-rebase tip is in the reflog:

git reflog show feat/x
# a1b2c3d feat/x@{0}: rebase finished: returning to refs/heads/feat/x
# e4f5g6h feat/x@{1}: rebase: applying commit 2
# ...
# d9e8f7c feat/x@{N}: branch: Created from <SHA>      ← this was the tip before rebase

git reset --hard d9e8f7c         # restore pre-rebase state

4. Pushed something you shouldn't have

You force-pushed a bad rebase to origin. Local reflog has the old SHAs; teammates' reflogs may also have them; restoration is the same git reset --hard <old-sha> followed by git push --force-with-lease. (Coordinate with the team — if anyone has based work on the bad push, they'll need to reset too.)

The 30-90 day window

Git's garbage collector eventually removes orphaned objects. Defaults:

  • gc.reflogExpire — 90 days for reachable commits in the reflog. The reflog entries themselves get pruned after this.
  • gc.reflogExpireUnreachable — 30 days for entries pointing at commits not reachable from any ref. (Newer git uses different exact defaults; check git config --get gc.reflogExpireUnreachable.)

Until GC runs, git fsck --lost-found can find commits with no ref pointing at them:

git fsck --lost-found
# Checking object directories: 100% (256/256), done.
# dangling commit a1b2c3d4e5f6...
# dangling commit f7e8d9c0b1a2...

# Inspect each:
git show a1b2c3d4
# if this is what you want:
git branch recovered a1b2c3d4

The 30-day window is a comfortable buffer. The discipline that matters: when you realize you've lost something, do recovery TODAY, not next month. GC could run in the meantime.

When reflog can't help you

  • Files you never staged or committed. If you rm -rf src/ before committing, git never saw those files — there's nothing to recover. (Your editor's local history might. VS Code has Timeline; JetBrains has Local History.)
  • Commits in a clone you deleted. Reflog is per-clone. If you rm -rf myrepo/, the reflog is gone. Either re-clone (you only get what's pushed) or pull from a teammate.
  • Force-push that overwrote shared history, and nobody else has the old. The remote no longer has it; if no clone has it, gone.

Useful aliases

# in ~/.gitconfig
[alias]
    rl = reflog --date=iso
    last = reflog -n 20 --date=relative

git last becomes a quick "what have I been doing?" view.

Recovery as everyday hygiene

The reflog is most useful when you treat it as a normal tool, not an emergency one. Two habits worth adopting:

  1. Before any destructive operation, jot down the current SHA. git rev-parse HEAD gives you the SHA. Paste into a scratch file. If the operation goes wrong, git reset --hard <that-sha> is one line.

  2. Never panic — always check reflog first. It's such an underused tool that engineers spend 20 minutes Googling and asking on Slack before remembering it exists. The first move when something feels lost: git reflog. You'll usually see the answer within 5 lines.

A complete recovery transcript

Here's a real recovery scenario, end to end:

# I just ran a bad rebase that lost 4 commits worth of work
$ git log --oneline | head
e4f5g6h (HEAD -> feat/x) refactored search
d4e5f6a use new client API

# Where were the commits I expected? Check reflog:
$ git reflog
e4f5g6h HEAD@{0}: rebase finished: returning to refs/heads/feat/x
d4e5f6a HEAD@{1}: rebase: refactored search
c3d4e5f HEAD@{2}: rebase: use new client API
b2c3d4e HEAD@{3}: pull: Fast-forward
a1b2c3d HEAD@{4}: commit: add tests for search          ← here it is, before the rebase
9876fed HEAD@{5}: commit: WIP search
8765edc HEAD@{6}: commit: extract SearchClient
7654dcb HEAD@{7}: commit: refactor search.go
6543cba HEAD@{8}: branch: Created from main

# Ah, the branch tip before the rebase was a1b2c3d
$ git reset --hard a1b2c3d
HEAD is now at a1b2c3d add tests for search

# Now I can re-attempt the rebase, more carefully this time
$ git rebase main

5 commands. 30 seconds. No work lost.

What to internalise

  • The reflog logs every move HEAD makes. Local-only, per-clone.
  • Reflog entries last 30+ days; orphan commits last ~90 days.
  • The recovery one-liner for almost every accident: git reflog → find the SHA → git reset --hard <sha> (or git branch <name> <sha>).
  • git fsck --lost-found finds orphans the reflog has forgotten.
  • The mental model: nothing in git is truly lost until GC runs. You almost always have a window.

Tools in the wild

4 tools
  • gitfree tier

    `git reflog` and `git fsck --lost-found` ship with every install.

    cli
  • Lazygitfree tier

    TUI shows reflog as a navigable list — much easier than scrolling text output.

    cli
  • tigfree tier

    Text-mode interface for git including reflog browser.

    cli
  • GitButlerfree tier

    Modern git client with point-in-time recovery via reflog visualisation.

    cli