Making git bisect more useful
If you've ever used git bisect
, you know what an incredibly useful tool this is. It allows you to do a binary search through commits to find out which commit caused a particular error. Many people seem unaware of git bisect run ...
which automates this even further, but it has a limitation: it won't let you find a particular error, it detects success or failure, that's all. So I decided to do something about that.
But first, let's have a quick review of git bisect
for those who don't know about it.
I use it a lot with testing. I'll fetch my changes from the master branch and run the test suite. If a test fails, I then do this from the root directory of my repository (that's required by git bisect
):
git bisect start
git bisect bad
git checkout HEAD~20
prove ...
git bisect good
Those translate to:
- Start bisecting
- Tell
git
this is a bad commit - Go far enough back in history to find a commit where the test passes
- Rerun the test to make sure the test passes (very important)
- Tell
git
that this is a good commit
At this point, git
will automatically check out a commit halfway between the two commits. You rerun your test and if it passes, you type git bisect good
. If it fails, you type git bisect bad
. (There's a whole lot more that I'm not covering here). Even if you don't have a test to run, you can use git bisect
and, for example, refresh your browser to look for that display bug you're trying to track down and then return to the command line and mark it good or bad as appropriate.
Repeat this process a few times and eventually git bisect
will tell you which commit first introduced the failure. Then you type git bisect reset
to stop bisecting.
However, I hate effectively typing this:
git bisect good
prove t/test/that/fails
git bisect bad
prove t/test/that/fails
git bisect bad
prove t/test/that/fails
git bisect bad
prove t/test/that/fails
git bisect bad
prove t/test/that/fails
git bisect bad
prove t/test/that/fails
git bisect good
prove t/test/that/fails
git bisect good
prove t/test/that/fails
git bisect bad
No matter how wonderful git bisect
is, I hate the boring repetition. However, you can automate this away, too. After you start your bisect and mark your starting and ending commits as good and bad, you can then do this:
git bisect run prove t/test/that/fails
And git bisect
will happily run the test for you and mark commits good or bad as appropriate, using the exit code of the program. You just sit back and wait for it to finish. Much nicer.
There are a couple of cases where this doesn't work, though. One case, obviously, is where you must manually verify good/bad and can't write a program to do this. Another case is where sometimes the test fails in different ways. When you do this manually, you can use git bisect skip
to skip a commit (or various commits). However, this means falling back to the tedious manual usage of git bisect
. Thus, I hacked together the following proof of concept:
#!/usr/bin/env perl
use strict;
use warnings;
use Getopt::Long;
use IPC::Open3;
use Symbol qw(gensym);
GetOptions(
'contains=s' => \my $contains,
'matches=s' => \my $matches,
) or usage();
usage() unless $contains xor $matches;
usage() unless @ARGV;
my $command = join ' ' => @ARGV;
my $stderr = gensym;
my $pid = open3( gensym, ">&STDERR", $stderr, $command );
my $output = '';
while ( my $err = <$stderr> ) {
print STDERR $err;
$output .= $err;
}
my $failed = $matches
? ( $output =~ qr/$matches/ )
: ( index( $output, $contains ) > -1 );
waitpid( $pid, 0 );
exit $failed;
sub usage {
die <<"END";
Usage:
$0 --contains 'exact text to fail on' command to run
$0 --matches 'pattern to fail on' command to run
END
}
I dropped that into my path and named it fail_if
. You pass it a string of text to look for (--contains
for an exact match or --matches
for a regex match). If that text is found in the programs STDERR
, the fail_if
exits with a non-zero exit status. Otherwise, it exits with 0 (success). You use it like this:
git bisect run fail_if --contains "Field 'user' doesn't have a default value" \
prove t/path/to/test/that/sometimes/fails/in/different/ways.t
And now, git bisect
will consider a commit a failure if that particular string is found in the output. So far it's allowed me to track down some fairly messy problems.
It probably can use a whole bunch of TLC (such as automatically skipping commits that fail in other ways). Suggestions welcome.
Thank your for the post!
I was using git bisect (and some time ago I was using svn bisect — https://metacpan.org/release/App-SVN-Bisect)
But after I have started using Jenkins for private project and Travis for open source project I have never used bisect tool.
bisect
has specific support for this situation: if the invoked process exits with status 125, the revision is considered untestable andbisect run
essentially invokesbisect skip
.Nice! I'm using this, with Masklinn's tip.
Great! one observation:
If your test command has options (eg. "test -v 2"), remember to add '--' after the "--matches ..." option to keep getopt calm. Eg.:
git bisect run fail_if --matches foo -- mytest -v 2