Reflog, Bisect, and Blame: Recovering Lost Work and Finding Bugs
A "lost" commit after a bad reset or force-push is almost never actually gone. git reflog keeps a local record of everywhere HEAD has pointed, bisect turns "somewhere in these 200 commits" into a 10-second binary search, and blame turns "who wrote this" into an actual answer.

Someone on my team once ran git reset --hard one commit too far, watched two hours of work vanish from git log, and started re-typing it from memory before I stopped them.
git log shows the history of your current branch. It does not show everywhere HEAD has ever been.
That second thing is git reflog. It often gives you a way back after commands from the previous chapter that look destructive, as long as the relevant entry and object have not expired or been pruned.
git reflog — the safety net under everything
Every time HEAD moves — a commit, a checkout, a reset, a rebase, a merge — Git records that move in the reflog. It's a purely local, personal log of your own repository activity.
- Never pushed, never shared
- Expires after ~90 days by default
git reflog
a1b2c3d HEAD@{0}: reset: moving to HEAD~1
e4f5g6h HEAD@{1}: commit: Add password reset flow
i7j8k9l HEAD@{2}: checkout: moving from main to feature-reset
m0n1o2p HEAD@{3}: commit: Refactor auth middleware
Reading top to bottom: most recent action first.
Recovering a "lost" commit
In the reset-gone-wrong scenario, e4f5g6h HEAD@{1} is the commit that "disappeared" — the branch pointer just moved away from it, but the commit object is still there.
Getting it back:
# Option 1: move the branch pointer back to it
git reset --hard HEAD@{1}
# Option 2: apply just that commit's changes (no branch pointer change)
git cherry-pick e4f5g6h
The same recovery works after:
-
A bad rebase (reflog records the branch tip from before the rebase started)
-
An accidental branch deletion (reflog remembers the branch's last position even after the label is gone)
-
A force-push that "erased" commits (they're still in the local object database)
-
This changes how cautious you need to be.
reset --hard, force-pushes, and rewritten rebases are recoverable locally far more often than they first appear. Treat reflog as a recovery option, not as a backup strategy. -
The limit: Reflog is local, can expire, and does not protect what other people already have on their machines. A shared branch rewritten after teammates pulled it needs coordination even when you can restore your own copy.
Detached HEAD — not an error, just a mode
Checking out a specific commit instead of a branch:
git checkout a1b2c3d
Produces a "detached HEAD" message. This looks alarming the first time.
In normal mode, HEAD points at a branch name, and the branch name points at a commit. In detached HEAD, HEAD points directly at a commit with no branch label in between. You can look around, run the code at that exact point in history, even make new commits — but those commits aren't attached to any branch. If you switch away without creating a branch first, they become orphaned.
The fix is simple:
git switch -c investigate-old-bug
Run this the moment you're in a detached HEAD state and want to keep anything you do there — it creates a real branch pointing at your current commit.
- You'll hit this when: You run
git checkout <commit-hash>to debug an old state, or aftergit bisect(which checks out commits automatically during the search).
git bisect — binary search through history to find a bug
"Somewhere in the last 200 commits, this stopped working" is a common and miserable debugging starting point.
Checking each commit one at a time is linear — 200 checks in the worst case.
bisect is a binary search over your history — 200 candidates becomes about 8 checks (log₂ of 200).
git bisect start
git bisect bad # current commit is broken
git bisect good v1.4.0 # this older tag/commit was known good
Git checks out a commit roughly halfway between the two. You test it:
git bisect good # this commit works
# or
git bisect bad # this commit is also broken
Each answer eliminates half of the remaining candidates. After about 8 rounds (log₂ of 200), Git identifies the exact first commit where things broke.
When done:
git bisect reset # returns you to where you started
Automating bisect with a test script
If you have a test that fails on the broken commit:
git bisect run ./test.sh
Git does the entire binary search unattended. The full search runs in the time it takes your test suite to run ~8 times — not 200.
- You'll use this when: A bug report says "this worked in v1.2 but not now" and you have no idea which of the 150 commits between them is the culprit.
git blame — who changed this line, and why
git blame utils.js
Annotates every line with the commit hash, author, and date of the last change:
a1b2c3d (Naveen 2026-08-05 14:23:11) const MAX_RETRIES = 3;
e4f5g6h (Alice 2026-08-10 09:41:05) const TIMEOUT = 5000;
The real use isn't "who do I blame" — it's "what commit was this change part of, so I can read the full context":
git show a1b2c3d # see the full commit that changed this line
Getting past reformatting noise
If a line was last touched by a large reformat or mass find-and-replace, blame will point at that reformat commit instead of whoever actually wrote the logic.
git blame -w utils.js # ignore whitespace-only changes
git log --follow -p -- utils.js # full history including renames
-w filters whitespace-noise attribution. --follow walks file history through renames — both help you get to the commit that actually matters.
Quick reference
| Problem | Command |
|---|---|
| "I think I lost a commit" | git reflog → find the hash → git reset --hard <hash> |
| "I accidentally deleted a branch" | git reflog → git branch recovered <hash> |
| "I'm in detached HEAD state" | git switch -c <name> (if you want to keep commits) |
| "Which commit broke this feature?" | git bisect start → git bisect bad/good → git bisect reset |
| "Who wrote this line and why?" | git blame <file> → git show <hash> |
git reflog is the command I tell every developer to memorize first, before they ever need it. It's the difference between "I lost two hours of work" and "oh, here it is."
The next chapter moves to remotes: what origin actually is, the real difference between fetch and pull, and how the fork-and-PR workflow fits together as one coherent system.
Enjoyed this chapter?
Get an email when I publish the next chapter. No spam — just new technical deep-dives.
Comments
Share feedback or questions about this blog post.
No comments yet. Be the first to share your thoughts.