Skip to content
Knowledge Hub

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.

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 --oneline
e4486e9 feat: add payment API
dd91a76 feat: add order API
2edc7b5 feat: app skeleton
$ git reset --hard HEAD~2 # oops: 2 commits vanish from the branch
HEAD is now at 2edc7b5 feat: app skeleton
$ git reflog -n 4
2edc7b5 HEAD@{0}: reset: moving to HEAD~2
e4486e9 HEAD@{1}: commit: feat: add payment API
dd91a76 HEAD@{2}: commit: feat: add order API
2edc7b5 HEAD@{3}: commit (initial): feat: app skeleton
$ git reset --hard 'HEAD@{1}' # go back to the position right before the reset
HEAD is now at e4486e9 feat: add payment API
$ git log --oneline
e4486e9 feat: add payment API
dd91a76 feat: add order API
2edc7b5 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 restore and reset --hard really 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 --oneline
6e3c8ed docs: changelog 6
ff94529 docs: changelog 5
b824065 docs: changelog 4
375576b refactor: extract the rounding step
a203b7a docs: changelog 3
ccca1c0 docs: changelog 2
93a4098 docs: changelog 1
961be8e feat: compute price after 10% discount
$ git bisect start HEAD v1.0
Bisecting: 3 revisions left to test after this (roughly 2 steps)
[a203b7a5538e8e6c4a0b22e25ec0ead9bb96a012] docs: changelog 3
$ git bisect run sh ../check.sh
running 'sh' '../check.sh'
Bisecting: 1 revision left to test after this (roughly 1 step)
[b824065e9f27c18b15c4f035723cba5fac9fe1b5] docs: changelog 4
running 'sh' '../check.sh'
Bisecting: 0 revisions left to test after this (roughly 0 steps)
[375576bfcca8253835e23ee6f30ab0ff26feea0d] refactor: extract the rounding step
running 'sh' '../check.sh'
375576bfcca8253835e23ee6f30ab0ff26feea0d is the first bad commit
commit 375576bfcca8253835e23ee6f30ab0ff26feea0d
Author: 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 reset
Previous HEAD position was 375576b refactor: extract the rounding step
Switched 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.

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 125 when npm ci or 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.

The script creates its own repo in a temp directory, does not touch the current repo, and cleans up when done:

Terminal window
bash examples/src/git-workflow/demo.sh

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