Git: Find the First Bad Commit With a One-Liner

Git: Find the First Bad Commit With a One-Liner

Scenario: a test fails on your current commit and you have no idea when it broke. This one-liner searches the whole Git history and prints just the hash of the commit that introduced the problem.

git bisect start HEAD $(git rev-list --max-parents=0 HEAD) > /dev/null 2>&1 && git bisect run ./test.sh 2>&1 | grep -i ' is the first bad commit' | cut -d " " -f1

Output:

7a9f828538ddf19ba23389630bdf56b3b8e91d03

The test script

test.sh is whatever tells a good commit from a bad one, and Git only looks at its exit code. Remember to chmod +x it.

#!/bin/sh
make || exit 125   # can't build: skip this commit
make test          # 0 = good, anything else = bad
  • 0: this commit is good
  • 125: this commit can't be tested, skip it
  • 1 to 127, except 125: this commit is bad

How it works

  • git rev-list --max-parents=0 HEAD: finds the commit with no parents, the very first one in the repo.
  • git bisect start HEAD <first-commit>: starts the search with the bad commit first and the good one second. The > /dev/null 2>&1 hides its chatter.
  • git bisect run ./test.sh: checks out the commit in the middle, runs the script, and halves the range based on the result. 1,000 commits take about 10 runs.
  • 2>&1: sends the errors from the test runs down the pipe too, so they don't end up on your screen.
  • grep -i ' is the first bad commit': keeps the one line with the answer, <hash> is the first bad commit.
  • cut -d " " -f1: keeps the first field of that line, the hash.

Before you run it

  • Check both ends yourself. Git never runs the test on HEAD or on the first commit, it just believes they are bad and good. If the bug was there from the start, you get the second commit as the culprit, and that is wrong.
  • Keep test.sh out of the history. Every checkout swaps the tracked files, so if the script was committed recently, older commits don't have it and the bisect stops. Leave it untracked, or put it in /tmp and use the full path.
  • Don't remove the exit 125. A commit that doesn't build says nothing about your bug. Count it as bad and Git can blame the wrong commit.
  • No output means no answer. Usually the script went missing, or the commits around the bug were all skipped and Git can only give you a list of suspects. Run it without the grep and cut to see why.
  • It doesn't take you back. When it finishes you are still bisecting, on a detached HEAD. Run git bisect reset, or add it to the end of the one-liner:
git bisect start HEAD $(git rev-list --max-parents=0 HEAD) > /dev/null 2>&1 && git bisect run ./test.sh 2>&1 | grep -i ' is the first bad commit' | cut -d " " -f1; git bisect reset > /dev/null 2>&1

$ ./buy-me-a-coffee ☕ // found this useful? I write these in my spare time