Git Recovery
Find a lost commit, isolate a regression, and attribute a line of history without treating blame as a personnel tool.
Three recovery jobs
Git Reset taught how refs, index, and working files move. Git Internals showed that a commit can still exist after a branch stops naming it. Recovery starts from those models and separates three different questions.
“Where did my commit go?”
Inspect a local reflog, verify the object, and give the wanted commit a durable branch name.
“Which commit introduced this regression?”
Give git bisect one known-good boundary, one known-bad boundary, and the same test at every midpoint.
“Why is this line here?”
Use git blame to find a last-touch commit, then read that commit and neighboring history.
“How do I fix it?”
Only after diagnosis. Recovery identifies and preserves evidence; the repair may be a new commit, revert, merge, or coordinated rewrite.
Stop and preserve evidence
# Stop changing the repository. Record the current evidence.
git status --short --branch
git log --oneline --decorate --graph --all -12
git reflog --date=iso -12
# Verify candidates before creating any recovery name.
git show --stat <candidate>
git cat-file -t <candidate>Status answers whether index or working-tree content is at risk. The graph shows refs that still reach commits. Reflog shows recent local movement. git show verifies a candidate before you attach a new name. Read-only commands come first because every new checkout, reset, rebase, or commit adds more history to interpret.
Often recoverable by name
- Find the object in a ref or reflog.
- Verify its tree and message.
- Create a recovery branch.
- Compare before integrating.
Not promised by Git
- Reflog records ref updates.
- It is not editor history.
- Untracked files may never be objects.
- Hard reset can overwrite tracked edits.
Recover a displaced commit with reflog
A reflog entry says a ref used to have a value. HEAD@{1} means the previous recorded value of HEAD; main@{1} means the previous recorded value of the local main ref. Their numbers are relative and can change as more entries arrive, so copy and verify the full object id before a long investigation.
# Example: reset or rebase moved the branch away from wanted commits.
git reflog --date=iso main
git show --stat main@{1}
# Preserve first. This does not move main or change the working tree.
git branch recovery/lost-tip main@{1}
git log --oneline --decorate --graph --all
# Compare before deciding whether to merge, cherry-pick, or reset.
git log --left-right --oneline main...recovery/lost-tip
git diff main...recovery/lost-tipTest what you learned
Type the command that preserves the verified commit at HEAD@{1} as recovery/lost-tip.
The recovery branch is the safety result. It turns a temporary reflog clue into a normal ref. You can now inspect, diff, merge, cherry-pick, or leave the old series alone without guessing. Do not immediately reset main to the candidate: another branch may already contain wanted later work.
Know what reflog cannot recover
Local only
Another clone has a different reflog. A fresh clone does not inherit yours. GitHub does not host your local reflog.
Temporary
Entries expire and unreachable objects may later be pruned. A recovery branch is durable; a reflog selector is not a backup plan.
Refs, not keystrokes
A file version never staged, committed, or stashed may have no Git object. Check editor or filesystem recovery separately.
Not permission to rewrite shared work
Finding an old tip does not justify force-pushing it. Preserve locally and choose a collaboration-safe repair.
If no ref or reflog names the commit, git fsck --no-reflogs --unreachable can report unreachable objects still present locally. That is last-resort read-only inventory, not the first step. Verify every candidate with git show and name wanted commits. Do not run aggressive garbage collection during an incident.
Define the bisect contract
git bisect checks commits between a known good ancestor and a known bad descendant. Git chooses a midpoint; you run one test and mark that commit good or bad. Repeating this cuts the search space roughly in half. A thousand-commit range needs about ten classifications, not a thousand guesses.
Known bad
The bug reproduces now with exact input and expected failure. “Someone reported it” is not a test.
Known good
An older commit passes the same check in a historically valid environment.
One classification
Good means the target behavior passes. Bad means the same behavior fails. Do not change definitions halfway through.
Untestable
Use git bisect skip when a historical commit cannot build for an unrelated reason. Skips may leave several possible culprits.
Run a manual bisect
# Start from a reproducibly bad commit.
git status --short
git bisect start
git bisect bad HEAD
git bisect good <known-good-commit>
# At each commit, run the same check.
npm test
git bisect good # only when the check passes
git bisect bad # only when the check fails
# After Git identifies the first bad commit:
git show --stat
git bisect resetTest what you learned
Type the command that ends bisect mode and restores the checkout from before the search.
Bisect temporarily checks out commits, usually with detached HEAD. Keep the working tree clean before starting. Do not edit and commit fixes during classification. When Git reports the first bad commit, record its full SHA and inspect it, then run git bisect reset before making a repair branch.
Merge-heavy history can produce more than one meaningful path. Default bisect follows ancestry between the boundaries and identifies where the test first becomes bad in that graph. If the result is a merge commit, inspect both parents and the merge diff instead of assuming one author introduced the behavior.
Automate a reliable bisect
# The script must return:
# 0 known good
# 1-127 (except 125) known bad
# 125 cannot test this commit
git bisect start HEAD <known-good-commit>
git bisect run ./scripts/repro.sh
# Read the reported first bad commit, then restore the original checkout.
git show --stat <first-bad-commit>
git bisect resetAn automated script must be executable, deterministic, and compatible with the historical range. It should create temporary output outside tracked paths or clean up after itself. It must not install unreviewed software, mutate refs, push, or depend on current uncommitted files.
One bounded behavior
- Same input at every commit.
- Clear pass/fail exit status.
- Historical setup is documented.
- Runs without network when possible.
Moving target
- Reads today’s working tree.
- Depends on flaky services.
- Rewrites files or refs.
- Calls every build failure “the regression.”
Exit 125 tells Git this commit cannot be classified. Use it only for genuinely untestable points. If many commits are skipped, the result may be a range of candidates instead of one commit. Report that uncertainty honestly.
Use blame without blaming people
# Ask which commit last changed a bounded range.
git blame -L 10,25 -- src/parser.js
# Ignore whitespace-only changes when that matches the investigation.
git blame -w -L 10,25 -- src/parser.js
# Then inspect intent and surrounding history.
git show <commit> -- src/parser.js
git log --follow --oneline -- src/parser.jsTest what you learned
Type the command that attributes lines 10 through 25 of src/parser.js.
git blame maps each selected line to the commit that last changed it. It does not prove who designed the behavior, who reviewed it, who was under an incident constraint, or which commit first made a test fail. Use the SHA as a doorway to git show, the commit message, linked discussion, and surrounding history.
Bound the question
Use -L to inspect relevant lines instead of dumping an entire file.
Ignore noise deliberately
-w can ignore whitespace. Record that choice; it changes attribution.
Follow the path
git log --follow helps with a simple rename. Copy detection options can help, but results remain heuristics.
Talk about commits
Say “commit X last changed this line.” Do not turn a history command into a label for a teammate.
Guided practice: preserve and isolate
- 01Build a disposable history
Create eight small commits. Make commit five introduce a deterministic failing check; keep commits six through eight unrelated.
- 02Displace and recover the tip
Create a practice branch, move it back two commits with mixed reset, find the old tip in that branch’s reflog, verify it, and create
recovery/lost-tip. - 03Bisect the regression
Mark commit one good and the recovered tip bad. Run the same check until Git identifies commit five. Reset bisect mode.
- 04Investigate the line
Use bounded blame on the failing behavior, then
git showthe last-touch commit. Compare that evidence with the bisect result. - 05Write the report
Record displaced tip, recovery ref, good/bad boundaries, first bad SHA, blame SHA, and why those last two may answer different questions.
Independent lab: recovery report
- In a disposable local repository you own, create at least ten commits. One middle commit must introduce a deterministic regression; later commits must preserve it. Record the known-good and known-bad SHAs.
- On a separate practice branch, move the tip backward. Use the branch-specific reflog, verify the old object with
git show, and preserve it asrecovery/lost-tip. Do not reset the branch forward as your first recovery action. - Run a manual bisect from the known-bad tip to the known-good ancestor. Record every tested SHA and classification. Use
git bisect skipfor one deliberately untestable historical commit only if you can explain why. - Run
git bisect reset. Use boundedgit blame,git show, and path history on the line involved. Do not name a person as “the cause.” - Write eight evidence lines: incident symptom, displaced tip, recovery ref, known-good SHA, known-bad SHA, first-bad result (or candidate range), last-touch blame SHA, and the repair strategy you would choose without force-pushing.
- Finish with clean status and a graph where every wanted commit is reachable by a normal ref. Do not run pruning, do not edit
.git, and do not push this exercise.
Common recovery mistakes
Resetting before preserving
More ref movement makes the incident harder to explain. Create a recovery branch first.
Expecting reflog on another clone
Reflogs are local. Ask what that clone fetched or recorded instead.
Bisecting a flaky symptom
Inconsistent classifications can identify the wrong boundary. Stabilize the check first.
Forgetting bisect reset
You remain detached at a historical commit. End the search before starting the fix.
Calling every build failure bad
If the target behavior cannot run for another reason, skip; do not change the question.
Using blame as accusation
It reports last-touch commits. It does not report intent, ownership, review context, or fault.
Lesson review
You can recover a displaced commit by verifying and naming it, isolate a reproducible regression with manual or automated bisect, and use blame to enter the surrounding history. GitHub Security keeps incident evidence from repeating an exposed credential. The team CI project stays planned.
- I stop mutating, record evidence, and create a recovery branch before choosing a repair.
- I know reflog is local, temporary ref history—not a backup of every file version.
- I can define good, bad, and untestable results and finish with
git bisect reset. - I use bounded blame plus show and log, and I discuss commits rather than assigning personal fault.