Find the Commit That Broke It.
git bisect, by Hand and by Script

A check that passed at v1.0 fails today. In between there are thirteen commits.
$ sh check.sh; echo $?
1
The hashes in this article, such as e2b28b4, come from our own run. Yours will differ, so match commits by their messages.
You could read thirteen diffs. You could check out each commit and run the check. Or you can let Git do the searching, and tell it only whether a commit is good or bad.
git bisect finds the commit that broke the check. One command answers the question without any help from us:
$ git bisect run sh check.sh
...
e2b28b48490fa9fca263b1139911de5315b6778c is the first bad commit
A project with one broken commit#
Our scratch project is a single script, bill.sh, which multiplies a quantity by a price. check.sh tests it:
#!/bin/sh
# exits 0 when 3 items at 4 each total 12
[ "$(sh bill.sh 3 4)" = "12" ]
The exit code is the whole result. 0 means the check passed, anything else means it failed. That is the contract bisect uses, so keep it in mind.
The history has sixteen commits, and v1.0 is a tag on the third:
$ git log --oneline
aa1dcd3 Invite contributions
0f1c2ac Add plums
9a951ee Note whole-number prices
e1eb7c8 Explain item list
977c5b3 Add cherries
e2b28b4 Add loyalty discount
78bad62 Mention check.sh
189fe51 Comment the formula
97330b4 Add bananas
1bf0dc7 Add license
500813b Ignore log files
32b8a71 Add pears
ab70081 Add item list
f81a834 Document usage
fa1d2e4 Add README
60d1b2a Add bill script
Most of these commits touch a README or a list of fruit. One of them changed the arithmetic. The output in this article comes from Git 2.45.2 on Windows 11, run in Git Bash.
Build the project yourself (Git Bash, optional)
Your hashes will differ from the ones printed here. The history, the tag and the results will match.
git init -b main bill && cd bill
c() { git add . && git commit -m "$1"; }
printf '#!/bin/sh\necho $(( $1 * $2 ))\n' > bill.sh
printf '#!/bin/sh\n# exits 0 when 3 items at 4 each total 12\n[ "$(sh bill.sh 3 4)" = "12" ]\n' > check.sh
c "Add bill script"
echo "# Bill" > README.md; c "Add README"
echo "Usage: sh bill.sh QUANTITY PRICE" >> README.md; c "Document usage"
git tag v1.0
echo "apple 4" > items.txt; c "Add item list"
echo "pear 5" >> items.txt; c "Add pears"
echo "*.log" > .gitignore; c "Ignore log files"
echo "MIT" > LICENSE; c "Add license"
echo "banana 2" >> items.txt; c "Add bananas"
printf '#!/bin/sh\n# total = quantity * price\necho $(( $1 * $2 ))\n' > bill.sh; c "Comment the formula"
echo "Run check.sh to test." >> README.md; c "Mention check.sh"
printf '#!/bin/sh\n# total = quantity * price, less a loyalty discount\necho $(( $1 * $2 - 1 ))\n' > bill.sh; c "Add loyalty discount"
echo "cherry 9" >> items.txt; c "Add cherries"
echo "Items live in items.txt." >> README.md; c "Explain item list"
printf '#!/bin/sh\n# total = quantity * price, less a loyalty discount\n# prices are whole numbers\necho $(( $1 * $2 - 1 ))\n' > bill.sh; c "Note whole-number prices"
echo "plum 3" >> items.txt; c "Add plums"
echo "Contributions welcome." >> README.md; c "Invite contributions"
Search, do not read#
Bisect is a binary search. We tell Git one commit that is bad and one that is good. Git checks out the commit in the middle, and we say whether it is good or bad. Whatever we answer throws away half of the commits that are left.
good ● ● ● ● ● ● ● ● ● ● ● ● ● ● bad
test here
Thirteen commits take about four tests. Sixty-four take about six, and a thousand take about ten. Every extra test doubles the number of commits we can cover.
By hand#
We start the session, then tell Git which end is which. The commit we are standing on is bad, and the v1.0 tag was good:
$ git bisect start
status: waiting for both good and bad commits
$ git bisect bad
status: waiting for good commit(s), bad commit known
$ git bisect good v1.0
Bisecting: 6 revisions left to test after this (roughly 3 steps)
[189fe51b563dc16a0f96744518ba87072c7a5e36] Comment the formula
Git has now checked out 189fe51, in the middle. Our working folder holds that commit. We run the check:
$ sh check.sh; echo $?
0
$ git bisect good
Bisecting: 3 revisions left to test after this (roughly 2 steps)
[977c5b326b651d1920eaeb0f82eaec1458d313c1] Add cherries
It passed, so everything up to and including 189fe51 is good. Git jumped to the middle of what is left.
$ sh check.sh; echo $?
1
$ git bisect bad
Bisecting: 0 revisions left to test after this (roughly 1 step)
[e2b28b48490fa9fca263b1139911de5315b6778c] Add loyalty discount
$ sh check.sh; echo $?
1
$ git bisect bad
Bisecting: 0 revisions left to test after this (roughly 0 steps)
[78bad623c1471205a79ced65f082805a7e706ddd] Mention check.sh
Git's estimates say "roughly", and ours was off by one: it predicted three steps and we answered four times. The last commit it asks about is the one just before the suspect.
$ sh check.sh; echo $?
0
$ git bisect good
e2b28b48490fa9fca263b1139911de5315b6778c is the first bad commit
commit e2b28b48490fa9fca263b1139911de5315b6778c
Author: Test User <test@example.com>
Date: Mon Oct 26 19:23:19 2026 -0400
Add loyalty discount
bill.sh | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
Four tests, and the answer is Add loyalty discount. Here is what it did:
$ git show e2b28b4
...
-# total = quantity * price
-echo $(( $1 * $2 ))
+# total = quantity * price, less a loyalty discount
+echo $(( $1 * $2 - 1 ))
A discount of one unit, subtracted from every total.
Get back to your branch#
While bisecting, we are not on a branch:
$ git status
HEAD detached at 189fe51
You are currently bisecting, started from branch 'main'.
(use "git bisect reset" to get back to the original branch)
That is expected. When we are done, whether we found the answer or gave up, we leave:
$ git bisect reset
Previous HEAD position was 78bad62 Mention check.sh
Switched to branch 'main'
Start with a clean working folder. Bisect checks out other commits, so uncommitted work is in its way. Stash it first.
By script#
We answered good or bad four times by running the same command and reading the exit code. That is a job for a script. git bisect run does exactly that loop:
$ git bisect start HEAD v1.0
Bisecting: 6 revisions left to test after this (roughly 3 steps)
[189fe51b563dc16a0f96744518ba87072c7a5e36] Comment the formula
$ git bisect run sh check.sh
running 'sh' 'check.sh'
Bisecting: 3 revisions left to test after this (roughly 2 steps)
[977c5b326b651d1920eaeb0f82eaec1458d313c1] Add cherries
running 'sh' 'check.sh'
Bisecting: 0 revisions left to test after this (roughly 1 step)
[e2b28b48490fa9fca263b1139911de5315b6778c] Add loyalty discount
running 'sh' 'check.sh'
Bisecting: 0 revisions left to test after this (roughly 0 steps)
[78bad623c1471205a79ced65f082805a7e706ddd] Mention check.sh
running 'sh' 'check.sh'
e2b28b48490fa9fca263b1139911de5315b6778c is the first bad commit
...
bisect found first bad commit
git bisect start HEAD v1.0 is the shorthand for the first three commands: bad first, then good. Same commit as by hand, and we did not type an answer.
Git reads the exit code of the command after run. The manual defines it:
| Exit code | Meaning |
|---|---|
0 | The commit is good |
1 to 127, except 125 | The commit is bad |
125 | The commit cannot be tested, so skip it |
Anything else, such as 255 | Stop the whole bisect |
That means a script that cannot start looks like a bad commit. Exit code 127 is the shell's "command not found", and bisect counts it as bad. To see it, we ran a script name that does not exist:
$ git bisect run sh nothere.sh
running 'sh' 'nothere.sh'
sh: nothere.sh: No such file or directory
[f81a83459eb425d69352d2232cb15b127a31236d] Document usage
running 'sh' 'nothere.sh'
sh: nothere.sh: No such file or directory
error: bogus exit code 127 for good revision
Git moved down to v1.0, which we said was good, got a failure there too, and stopped. This is what you would see if check.sh was added to the repository after v1.0: older commits do not have it. Keep a test script outside the repository, or untracked, so it survives every checkout.
When a commit fails for a reason that is not our bug, such as a build that does not compile, the script should say so with 125:
make || exit 125
sh check.sh
Git skips that commit and picks another. By hand, git bisect skip does the same.
And we reset when finished:
$ git bisect reset
Previous HEAD position was 78bad62 Mention check.sh
Switched to branch 'main'
What bisect cannot tell you#
Bisect finds the first commit where the check changed from passing to failing. It does not explain why that change broke the check. Reading the diff is still our job. And it trusts our answers. If the check is flaky, or we mark one commit wrong, the search narrows on the wrong half and ends with a confident wrong commit. We have not tested what Git does on branches with many merges, so check that case on your own history before relying on it.
Try it#
Here is the same hunt as ten commands. Match each commit by its message, not its hash.
Type these commands in order, in a practice folder:
- Start the session. Try
git bisect start. - Mark the commit you are on as bad. Try
git bisect bad. - Mark the
v1.0tag as good. Trygit bisect good v1.0. Git checks out the commit in the middle,Comment the formula. sh check.shprinted 0 atComment the formula. Tell Git:git bisect good.- At
Add cherriesit printed 1. Tell Git:git bisect bad. - At
Add loyalty discountit printed 1. Tell Git:git bisect bad. - At
Mention check.shit printed 0. Tell Git:git bisect good. Git names the first bad commit,Add loyalty discount. - Get back to your branch. Try
git bisect reset. - Start again, with both ends in one command. Try
git bisect start HEAD v1.0. - Let a script do the answering. Try
git bisect run sh check.sh.
Two things worth trying: answer bad at step 4 and read what the terminal says about the exit code, and finish the lesson and count how many times the script ran.
Challenge: ask a different question
check.sh tests arithmetic, so it is no use here. In the same project, find the first commit after v1.0 where items.txt contains the word cherry. Use git bisect run with one inline command, and no script file.
Expected result: Git prints ... is the first bad commit, and the commit is Add cherries. Run git bisect reset afterwards.
Answer
$ git bisect start HEAD v1.0
$ git bisect run sh -c '! grep -q cherry items.txt'
...
Add cherries
...
bisect found first bad commit
At v1.0 there is no items.txt, so grep fails and ! turns that into a pass. That is the right answer for a commit that has no cherries. The quotes are for Git Bash.
What we learned#
git bisectis a binary search over history. We name one good and one bad commit, and Git checks out the middle.git bisect start,git bisect bad,git bisect good v1.0, thengoodorbadafter each check.- Git says "roughly" about the number of steps, and it is only a guide.
git bisect start HEAD v1.0followed bygit bisect run sh check.shdoes the whole search from one script.- The script's exit code is the answer:
0good,1to127bad except125, which skips the commit. - Keep the test script outside the repository or untracked, so every commit can run it.
git bisect resetalways ends the session and returns to the branch.
Comments require JavaScript.