Disaster recovery
When a reset, rebase, or branch delete seems to destroy work, escalate calmly: git reflog finds moved tips, git fsck --lost-found surfaces dangling objects, git cat-file -p inspects them. Aggressive git gc prunes unreachable objects. The only true loss is never-committed work.
You run git reset --hard HEAD~5 to undo a messy merge, glance at the screen, and realise the five commits you wanted are gone from the branch — an afternoon of work, apparently deleted. Or you delete a branch, then remember it had the only copy of a fix. The instinct is panic, but here is the reassuring truth: git almost never actually destroys a committed object the moment you “lose” it. The commit is still in the object store, just no longer pointed to — and there is a calm, ordered way to get it back.
By the end of this lesson you will have a disaster-recovery ladder you can climb in escalating order — reflog, then fsck, then inspecting and restoring dangling objects — and you will know the one true loss git cannot help with and the deadline that ends recovery.
After this lesson you can recover seemingly-lost commits in escalating order — git reflog, then git fsck --lost-found, then git cat-file -p to inspect and restore a dangling object — and explain how git gc sets the deadline after which unreachable objects are truly gone.
First rung: git reflog — almost every “lost” commit after a reset, rebase, or branch-delete is here. The reflog is a local journal of everywhere HEAD and your branch tips have pointed. A reset --hard, a botched rebase, even a branch -D all leave the old position recorded. Find it and reset back:
git reflog # list recent HEAD positions with SHAs
# e.g. abc123 HEAD@{1}: reset: moving to HEAD~5
git reset --hard abc123 # restore the branch to before the resetThis handles the overwhelming majority of self-inflicted accidents. The commits were never deleted — only the branch label moved — and the reflog remembers where it was. Try this before anything more exotic.
Second rung: git fsck --lost-found — find dangling objects when even the reflog cannot help. The reflog is per-clone and expires (default ~90 days for reachable, ~30 for unreachable entries); a fresh clone has none, and git gc can trim it. When reflog draws a blank, ask Git to scan the entire object database for objects nothing points to:
git fsck --full --unreachable # list all unreachable objects
git fsck --lost-found # write dangling commits/blobs to .git/lost-found/--lost-found drops dangling commit objects into .git/lost-found/commit/ and blobs into .git/lost-found/other/, named by SHA. A “dangling” commit is one no branch, tag, or reflog reaches — exactly what a deleted branch’s tip becomes. fsck finds it even when nothing remembers its name.
Third rung: git cat-file -p to inspect a candidate, then restore it. fsck gives you a list of SHAs but no context. Print any object to see whether it is the one you want before acting:
git cat-file -t <sha> # what kind of object is this? (commit/blob/tree)
git cat-file -p <sha> # print it: commit message, author, tree, parentOnce you have identified the right dangling commit, give it a name again so it stops being unreachable:
git branch recovered <sha> # re-anchor the lost commit on a branchFor a single lost file (a dangling blob), git cat-file -p <blob-sha> > recovered.txt writes its contents straight back to disk. Naming the object is what saves it: an object with a ref is reachable, and reachable objects are never pruned.
The deadline: git gc is what finally, permanently deletes unreachable objects. Recovery works because Git keeps unreachable objects around — but not forever. Garbage collection (git gc, run automatically by many commands) prunes objects that are both unreachable and older than a grace period (default two weeks). An explicit aggressive run skips much of the safety net:
git gc --prune=now # immediately delete ALL unreachable objectsThat is the point of no return: after gc --prune=now, a dangling commit that reflog already forgot is genuinely gone. So the recovery rule is act before gc. And one loss git can never undo: changes you never committed — unstaged or unsaved edits — are not objects at all, so reflog, fsck, and cat-file have nothing to find. Commit early; an object can be recovered, an un-saved edit cannot.
▸Why this works
Why can git recover a “deleted” commit at all? Because Git is content-addressed and append-only at the object level. Writing a commit stores an immutable object keyed by its SHA; operations like reset, rebase, and branch-delete only move refs (branch and HEAD pointers), they do not erase objects. A commit with no ref pointing at it becomes unreachable but still physically present in .git/objects. Reflog and fsck are simply two ways to rediscover the SHA of an object that lost its label. Garbage collection is the only thing that actually removes objects — which is why every recovery technique is really a race against the next gc.
A deleted branch, recovered two ways.
A developer finishes a tricky fix on hotfix-auth, then — thinking it was already merged — runs git branch -D hotfix-auth. It was not merged. The tip commit now has no ref pointing to it.
Easy path (reflog still has it). They run git reflog, spot def456 HEAD@{6}: checkout: moving from hotfix-auth to main, read the SHA of the deleted tip, and run git branch hotfix-auth def456. The branch is back, every commit intact, total elapsed time under a minute. Most recoveries end here.
Hard path (fresh clone, no reflog). Suppose this happened on a CI machine that then re-cloned, wiping the reflog. They run git fsck --full --no-reflogs --lost-found, which lists several dangling commits in .git/lost-found/commit/. They git cat-file -p each candidate, reading commit messages until they find “Fix auth token refresh” with the right author and date. They re-anchor it: git branch recovered-auth <sha>, then verify with git log. The work is back — but they note that had someone run git gc --prune=now first, the object would have been pruned and unrecoverable. The calm checklist held: reflog, then fsck, then cat-file, then a branch — and never panic-run gc.
▸Common mistake
The worst move during a recovery is to “tidy up” by running git gc --prune=now or git reflog expire --expire=now --all — exactly the commands that destroy the objects you are trying to save. Under stress people reach for cleanup, but recovery wants the opposite: leave the repo alone, stop running destructive commands, and work read-only (reflog, fsck, cat-file are all safe) until you have re-anchored the object on a branch. The second classic mistake is over-trusting recovery and not committing often enough — reflog and fsck only see committed objects, so the discipline that makes them powerful is committing your work in the first place.
You ran `git reset --hard HEAD~3` and lost three commits. The reflog still works. What is the correct first step?
When a reset, rebase, or branch -D seems to destroy committed work, recover in escalating order. git reflog is the first rung — it journals every place HEAD and your branches pointed, so you usually just reset --hard to the pre-accident SHA. When the reflog is gone (fresh clone, expired, or trimmed), git fsck --lost-found scans the whole object store for dangling objects nothing references. Use git cat-file -p to inspect a candidate SHA, then git branch <name> <sha> to re-anchor it — naming an object makes it reachable and safe. The deadline is git gc: an aggressive --prune=now permanently deletes unreachable objects, so always recover before gc and never panic-run cleanup commands. The one loss git cannot undo is work you never committed, because it was never an object. That is the whole of this rescue unit — and of the git track.
Practice
Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.
Something unclear?
Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.