Finding a regression after a release
A feature worked in the last tagged release and is broken on main. Start bisect with main as bad and the tag as good, then test each commit Git checks out until it names the culprit.
Git · Command
git bisect does a binary search through commit history to find the commit that introduced a bug. More generally, it finds the commit where any testable property of the project changed.
Last updated
You start a session and mark one commit as bad (it has the bug) and at least one as good (it does not). Git then checks out a commit about halfway between them. You test it and mark it good or bad, and each answer cuts the remaining range roughly in half. When no candidates are left, Git reports the first bad commit and points refs/bisect/bad at it. You can do the testing by hand or hand it to a script with git bisect run, which reads the script's exit code. When you finish, git bisect reset clears the bisect state and puts you back where you started.
git bisect start [--term-(bad|new)=<term-new> --term-(good|old)=<term-old>] [--no-checkout] [--first-parent] [--reset-when-found[=<where>]] [<bad> [<good>...]] [--] [<pathspec>...]
git bisect (bad|new|<term-new>) [<rev>]
git bisect (good|old|<term-old>) [<rev>...]
git bisect skip [(<rev>|<range>)...]
git bisect reset [<commit>]
git bisect run [--reset-when-found[=<where>]] <cmd> [<arg>...]| Name | Type | Description |
|---|---|---|
<bad> [<good>...] | commit-ish | Optional arguments to start: one known-bad commit followed by any number of known-good commits. Giving them here saves running separate bad/good commands. |
<pathspec> | path | Limits the search to commits that touch these paths, so fewer steps are needed. |
--term-old / --term-new | string | Your own names for the before and after states, such as fast/slow or broken/fixed. |
--no-checkout | flag | Does not check out each candidate. It only moves BISECT_HEAD to the commit being tested. This is assumed in bare repositories. |
--first-parent | flag | Follows only the first parent of merge commits, so a merge is reported as the culprit and the commits on the merged branch are ignored. |
--reset-when-found[=<where>] | original | found | After the culprit is found, report it and clean up the bisect state. The default, original, returns to the commit you started from; found leaves the culprit checked out. |
<cmd> [<arg>...] | command | For bisect run: a script or command whose exit code marks each commit as good, bad, or skipped. |
Prints a description of the first bad (or new) commit and leaves refs/bisect/bad pointing at it.
#!/usr/bin/env bash
# Save as ~/bisect-test.sh (keep it outside the repo) and chmod +x it
set -u
make >/dev/null 2>&1 || exit 125 # can't build: skip this commit
./run_regression_test.sh # exit 0 = good, 1 = bad
# --- Then, in the repository: ---
# git bisect start HEAD v1.2 -- # HEAD is bad, v1.2 is good
# git bisect run ~/bisect-test.sh
# git show refs/bisect/bad # inspect the culprit
# git bisect reset # return to the original HEADThe script skips commits that won't build by exiting 125 and otherwise reports the test's exit code, so bisect run can find the first bad commit without any manual steps.
A feature worked in the last tagged release and is broken on main. Start bisect with main as bad and the tag as good, then test each commit Git checks out until it names the culprit.
Start with --term-old broken --term-new fixed to find the commit that fixed a problem, for example to decide whether that fix should be backported.
Use bisect run with a benchmark script that exits non-zero when timings go over a threshold, and use the terms fast/slow so the output is easy to read.
It does a binary search over the commits that are reachable from the bad commit but not from any good commit. Each commit you test as good or bad removes about half of the remaining candidates, so the number of steps grows with the logarithm of the range. In the docs' example, about 675 candidate revisions need roughly 10 steps.
Follow-up: Does this work when history contains merges? Yes. Bisect works on the commit graph, not just a straight line of commits. If you want a broken merge reported as one culprit instead of commits on the merged branch, use --first-parent.
Run git bisect run <cmd> after setting good and bad bounds. Exit 0 marks the commit good. Exit 1 to 127, except 125, marks it bad. Exit 125 skips the commit, and any other code stops the bisect.
Follow-up: Why is 125 the skip code instead of 126 or 127? POSIX shells already use 126 and 127 for 'command found but not executable' and 'command not found', so 125 is the highest value that doesn't clash with them.
Save the output of git bisect log to a file and delete the wrong entry. Then run git bisect reset followed by git bisect replay <file>. This rebuilds the session with only the correct marks.
Follow-up: How do you leave the session but stay on the culprit commit? Run git bisect reset bisect/bad. Alternatively, start with --reset-when-found=found, which leaves the first bad commit checked out once the search ends.
git bisect rungit bisect skipgit bisect resetgit bisect loggit bisect replaygit bisect visualizegit blamegit rev-list --bisect