Git recovery playbook: reset, revert, reflog and bisect Intermediate
Estimated reading time: 7 min read
This page answers: which command to use when something goes wrong with Git, and how to find the commit that introduced a bug automatically. Both demos have real, reproducible output.
Choosing a command by situation
Section titled “Choosing a command by situation”Loading diagram…
| Situation | Command | Notes |
|---|---|---|
| Last commit is wrong, not pushed | git reset --soft HEAD~1 |
Drops the commit, keeps the changes staged. To fix only the message: git commit --amend |
| Discard uncommitted changes to a file | git restore <file> |
Restores from the index; uncommitted work is lost forever |
Accidentally ran git add |
git restore --staged <file> |
Only removes it from the index; the file in the worktree is untouched |
A commit pushed to main causes a bug |
git revert <sha> |
Creates a new inverse commit; no history rewrite. Reverting a merge: git revert -m 1 <merge> |
Lost commits after reset --hard, a botched rebase, a deleted branch |
git reflog → git branch rescue <sha> |
The reflog only exists in the local repo |
| Find the commit that introduced a bug | git bisect start <bad> <good> + git bisect run <cmd> |
Binary search; finish with git bisect reset |
| Mid-work, need to fix something else urgently | git stash push -m "..." → later git stash pop |
Add -u to stash untracked files too |
| Uncommitted code on the wrong branch | git switch -c feature/x |
Uncommitted changes come along to the new branch |
Accidentally committed to local main (not pushed) |
git branch feature/x → git reset --hard origin/main → git switch feature/x |
Check that git status is clean before reset --hard |
| Need one commit from another branch | git cherry-pick <sha> |
Creates a new commit with a new hash; use -x to record the source |
| Rebase/merge is a mess | git rebase --abort / git merge --abort |
Returns to the state before you started |
Once pushed to a shared branch, always revert, never reset. reset + force push forces everyone to resync and easily loses other people’s commits (see Common mistakes).
Demo 1: recovering commits lost to reset --hard with reflog
Section titled “Demo 1: recovering commits lost to reset --hard with reflog”The reflog records every change to a branch tip and to HEAD in the local repo. Default expiry:
| Entry type | Kept for | Config |
|---|---|---|
| Still reachable | 90 days | gc.reflogExpire |
| No longer reachable | 30 days | gc.reflogExpireUnreachable |
A “lost” commit stays in the object store until it expires and is cleaned up by gc.
Real output from examples/src/git-workflow/demo.sh (git 2.54.0):
$ git log --onelinee4486e9 feat: add payment APIdd91a76 feat: add order API2edc7b5 feat: app skeleton
$ git reset --hard HEAD~2 # oops: 2 commits vanish from the branchHEAD is now at 2edc7b5 feat: app skeleton
$ git reflog -n 42edc7b5 HEAD@{0}: reset: moving to HEAD~2e4486e9 HEAD@{1}: commit: feat: add payment APIdd91a76 HEAD@{2}: commit: feat: add order API2edc7b5 HEAD@{3}: commit (initial): feat: app skeleton
$ git reset --hard 'HEAD@{1}' # go back to the position right before the resetHEAD is now at e4486e9 feat: add payment API
$ git log --onelinee4486e9 feat: add payment APIdd91a76 feat: add order API2edc7b5 feat: app skeleton- Read the reflog top-down:
HEAD@{0}is now,HEAD@{1}is the step right before. - Safer than
reset --hard: create a branch at that hash (git branch rescue e4486e9), inspect it, then decide. - Limitation: the reflog cannot recover changes that were never committed (or never
git added).git restoreandreset --hardreally delete them.
Demo 2: git bisect run to find the commit that introduced a bug
Section titled “Demo 2: git bisect run to find the commit that introduced a bug”Scenario: price.sh computes the price after a 10% discount on an order of 200, expecting 180. Tag v1.0 is correct; HEAD is wrong; there are 7 commits in between. The check script lives outside the repo so every commit runs the same test:
#!/bin/sh# exit 0 = good, 1..127 (except 125) = bad, 125 = skip[ "$(sh ./price.sh)" = "180" ]Real output:
$ git log --oneline6e3c8ed docs: changelog 6ff94529 docs: changelog 5b824065 docs: changelog 4375576b refactor: extract the rounding stepa203b7a docs: changelog 3ccca1c0 docs: changelog 293a4098 docs: changelog 1961be8e feat: compute price after 10% discount
$ git bisect start HEAD v1.0Bisecting: 3 revisions left to test after this (roughly 2 steps)[a203b7a5538e8e6c4a0b22e25ec0ead9bb96a012] docs: changelog 3
$ git bisect run sh ../check.shrunning 'sh' '../check.sh'Bisecting: 1 revision left to test after this (roughly 1 step)[b824065e9f27c18b15c4f035723cba5fac9fe1b5] docs: changelog 4running 'sh' '../check.sh'Bisecting: 0 revisions left to test after this (roughly 0 steps)[375576bfcca8253835e23ee6f30ab0ff26feea0d] refactor: extract the rounding steprunning 'sh' '../check.sh'375576bfcca8253835e23ee6f30ab0ff26feea0d is the first bad commitcommit 375576bfcca8253835e23ee6f30ab0ff26feea0dAuthor: demo <demo@example.com>Date: Sun Oct 18 09:08:00 2026 +0700
refactor: extract the rounding step
price.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-)bisect found first bad commit
$ git bisect resetPrevious HEAD position was 375576b refactor: extract the rounding stepSwitched to branch 'main'- Three test runs for 7 candidate commits. Bisect is a binary search, so even 1,000 commits take only about 10 runs.
- A lesson on the side: the commit that introduced the bug is named
refactor— exactly the kind of commit that should not have changed behavior.
The git bisect run exit code convention
Section titled “The git bisect run exit code convention”Loading diagram…
| Exit code | Meaning |
|---|---|
0 |
good |
1–127, except 125 |
bad |
125 |
skip (this commit cannot be tested, e.g. it does not build) |
Other (e.g. 128–255) |
abort bisect |
A common trap: a script returning 127 (“command not found”) is treated as bad, and bisect points at the wrong commit. For a TypeScript project:
exit 125whennpm cior the build fails.- Only return bad when the exact test that reproduces the bug fails.
- Flaky tests make bisect point at the wrong commit (see flaky tests).
A demo of how this trap points at the wrong commit: Common mistakes.
Rerunning the demo
Section titled “Rerunning the demo”The script creates its own repo in a temp directory, does not touch the current repo, and cleans up when done:
bash examples/src/git-workflow/demo.shThe script pins the author name and commit dates, so the hashes match the output above with the same git version. The script writes its commit messages in Vietnamese; the messages above are translated, the hashes are from the real run.
Next: Secrets, PRs and checklist.