N
Naveenr.dev
Chapter 05
9 min read2026-08-09
📖 Git SeriesChapter 05 · 11 chapters

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.

Git reflog, bisect, and blame
Git reflog, bisect, and blame

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
bash
git reflog
text
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:

bash
# 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:

bash
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:

bash
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 after git 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).

bash
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:

bash
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:

bash
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:

bash
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

bash
git blame utils.js

Annotates every line with the commit hash, author, and date of the last change:

text
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":

bash
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.

bash
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

ProblemCommand
"I think I lost a commit"git reflog → find the hash → git reset --hard <hash>
"I accidentally deleted a branch"git refloggit 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 startgit bisect bad/goodgit 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.