# Find the Commit That Broke It

Canonical: <https://mnixblog.netlify.app/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.

```console
$ 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:

```console
$ 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:

```sh
#!/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:

```console
$ 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.

<details>
<summary>Build the project yourself (Git Bash, optional)</summary>

Your hashes will differ from the ones printed here. The history, the tag and the results will match.

```bash
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"
```


</details>

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

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

```console
$ 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:

```console
$ 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.

```console
$ 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.

```console
$ sh check.sh; echo $?
0
$ git bisect good
e2b28b48490fa9fca263b1139911de5315b6778c is the first bad commit
commit e2b28b48490fa9fca263b1139911de5315b6778c
Author: Test User <test@example.com>
Date:   Mon Sep 30 19:23:19 2024 -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:

```console
$ 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:

```console
$ 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:

```console
$ 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](https://mnixblog.netlify.app/advanced-git-features/) 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:

```console
$ 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:

```console
$ 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`:

```sh
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:

```console
$ 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.

**Practice terminal · hunt a bad commit**

Type these commands in order, in a practice folder:

1. Start the session. Try `git bisect start`.
2. Mark the commit you are on as bad. Try `git bisect bad`.
3. Mark the `v1.0` tag as good. Try `git bisect good v1.0`. Git checks out the commit in the middle, `Comment the formula`.
4. `sh check.sh` printed 0 at `Comment the formula`. Tell Git: `git bisect good`.
5. At `Add cherries` it printed 1. Tell Git: `git bisect bad`.
6. At `Add loyalty discount` it printed 1. Tell Git: `git bisect bad`.
7. At `Mention check.sh` it printed 0. Tell Git: `git bisect good`. Git names the first bad commit, `Add loyalty discount`.
8. Get back to your branch. Try `git bisect reset`.
9. Start again, with both ends in one command. Try `git bisect start HEAD v1.0`.
10. Let a script do the answering. Try `git bisect run sh check.sh`.

[Try the interactive Git practice terminal on the canonical article](https://mnixblog.netlify.app/find-the-commit-that-broke-it/).


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.

> **Tip:**
> Here, "bad" only means "has the thing we are looking for". So the command must exit `0` on commits that do not have it. `sh -c '...'` runs a one-line command. `!` flips an exit code, so a pass becomes a fail and the other way round. `grep -q` searches without printing anything, and exits `0` when it finds a match.

<details>
<summary>Answer</summary>

```console
$ 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.

</details>

**Question:** A script run by `git bisect run` exits with code 125. What does Git do with that commit?

1. It skips the commit, because it cannot be tested
2. It marks the commit as bad
3. It marks the commit as good

**Answer:** 1. It skips the commit, because it cannot be tested

> **Explanation:** `125` is the code reserved for "cannot be tested". The commit is skipped, and Git picks another to try.


**Question:** Roughly how many commits must bisect test to find the first bad one among 64 commits?

1. About 6
2. About 32
3. All 64

**Answer:** 1. About 6

> **Explanation:** Every test halves the commits that are left. 64 halves to 32, 16, 8, 4, 2, 1, which is six tests.


**Question:** What does `git bisect reset` do?

1. Ends the session and checks out the branch you started from
2. Marks the current commit as good
3. Reverts the bad commit

**Answer:** 1. Ends the session and checks out the branch you started from

> **Explanation:** `reset` only ends the search and takes you back. In our run it printed `Switched to branch 'main'`. It does not change any commit.


## What we learned

- `git bisect` is 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`, then `good` or `bad` after each check.
- Git says "roughly" about the number of steps, and it is only a guide.
- `git bisect start HEAD v1.0` followed by `git bisect run sh check.sh` does the whole search from one script.
- The script's exit code is the answer: `0` good, `1` to `127` bad except `125`, which skips the commit.
- Keep the test script outside the repository or untracked, so every commit can run it.
- `git bisect reset` always ends the session and returns to the branch.
> **Portable fallback:** The authored description of the demo is preserved; the live demonstration remains on the canonical article. Review: Review the authored description of the demo and keep the canonical link for the live version.

> **Portable fallback:** The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.

> **Portable fallback:** The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.

> **Portable fallback:** The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.

[Join the discussion on the canonical article](https://mnixblog.netlify.app/find-the-commit-that-broke-it/#ob-discussion)
