There are people out there who claim that merge-based workflows (that is, workflows which contain non-fast-forward merges) are bad.
They claim that git bisect
gets confused by merge-based workflows, and instead advocate rebase-based workflows without explicit feature branches.
They're wrong.
Furthermore, the "advantages" of their workflows are in fact disadvantages. Let me show you.
We're first going to see that git bisect
can, in fact, spot a fault commit in a merge-based worflow, and then we'll see why merge-based workflows offer better options for resolving the situation.
We're going to create a simple test repo, with one file which exits with 1 or 0, signifying a broken and non-broken commit respectively (this will be our test, which we pass to git bisect run
), and some unrelated files which we'll change to create some "padding" commits. See the transcript in the file below if you want the details.
I've replaced the commit hashes with letters for clarity.
C--D--------
/ \
A--B--E------M1--M2
\ /
F--G
^
faulty commit
Now we've got this history, let's see what git bisect
makes of it.
canton7@creek bisect_test (master) $ git bisect start M2 B
Bisecting: 2 revisions left to test after this (roughly 2 steps)
[M1] Merge branch 'feature_b'
canton7@creek bisect_test ((M1)|BISECTING) $ git bisect run ./test.sh
running ./test.sh
Bisecting: 1 revision left to test after this (roughly 1 step)
[F] F (break the test)
running ./test.sh
Bisecting: 0 revisions left to test after this (roughly 0 steps)
[E] E
running ./test.sh
F is the first bad commit
commit F
Author: canton7 <email>
Date: Mon Sep 17 13:50:07 2012 +0100
F (break the test)
:000000 100644 0000000000000000000000000000000000000000 5626abf0f72e58d7a153368ba57db4c673c0e171 A feature_b
:100755 100755 eecf7fdfc798a861a5faea10be05dae6b721a19b 275d27c3f381aa5bf18c9263e420e93c80515fcb M test.sh
bisect run success
# And reset the bisect
canton7@creek bisect_test ((7958de0...)|BISECTING) $ git bisect reset
Previous HEAD position was E... E
Switched to branch 'master'
Will you look at that? git bisect
found the faulty commit, confusing merges and all!
I've also tried it out on a number of other potentially confusing histories, and git bisect
has nailed it each time.
Now that we've identified the faulty commit, we've got a few options:
- Simply create a new commit which fixes the problem with the faulty one
- Revert the faulty commit (assuming it doesn't break the feature it's on), then repair it later
- Revert the entire feature (assuming it doesn't break other features), then repair it later
Option 1. is the quick and dirty solution, although it means that the commits which are to do with your feature are no longer confined to the feature branch.
Option 2. is probably not a good idea: the rest of the feature is very likely to break, and you've still got the problem from option 1.
Option 3. allows all of the commits relating to the feature branch to remain in a branch (we'll do an example in a minute), although you need to know what you're doing (and to have read this).
Note that with a rebase-and-ff-only workflow, option 3. is probably not available: you don't know where the feature branch was, so you can't revert it.
Let's do an example of option 3.
We'll first revert M1, which brought the faulty commit into master. This will give us some breathing space to come up wiht a fix. We'll then resurrect the feature branch, rebase it (otherwise the chanes from F and G won'y be re-applied when we merge in the resurrected branch, see this document), fix it, and merge it back in.
There are other workflows available here, including reverting the revert. Again, this document is your bible right now.
canton7@creek bisect_test (master) $ git revert -m 1 M1
[master W1] Revert "Merge branch 'feature_b'"
2 files changed, 1 insertion(+), 3 deletions(-)
delete mode 100644 feature_b
canton7@creek bisect_test (master) $ git checkout -b feature_b_repair M1^2
Switched to a new branch 'feature_b_repair'
canton7@creek bisect_test (feature_b_repair) $ git rebase -f E
Current branch feature_b_repair is up to date, rebase forced.
First, rewinding head to replay your work on top of it...
Applying: F (break the test)
Applying: G
canton7@creek bisect_test (feature_b_repair) $ echo -e '#!/bin/bash' "\nexit 0" > test.sh
canton7@creek bisect_test (feature_b_repair *) $ git commit -am "H (fix test)"
[feature_b_repair H] H (fix test)
1 file changed, 1 insertion(+), 1 deletion(-)
At this point, our history looks like this:
C--D--------
/ \
A--B--E------M1--M2--W1
|\ /
| F--G
\
F'--G'--H
If we wanted, we could git rebase -i E
and squash H info F', and maybe even rebase the whole thing onto W1. We'll skip that here for simplicity though.
At this point, all we need to do is to merge in the corrected feature branch:
canton7@creek bisect_test (feature_b_repair) $ git checkout master && git merge feature_b_repair
Switched to branch 'master'
Updating eba57aa..4c29869
Fast-forward
feature_b | 2 ++
1 file changed, 2 insertions(+)
create mode 100644 feature_b
to get our finished history:
C--D--------
/ \
A--B--E------M1--M2--W1--M3
|\ / /
| F--G /
\ /
F'--G'--H------
Thanks @n7800 for updated links!