You’re hacking along on your project, and you discover a bug (not in whatever feature you’re working on; somewhere else in the code). You’re using git for revision control, so branching and merging are very cheap, and it makes sense to create a bugfix branch. That way you need not worry about creating a pristine patch immediately – you can fearlessly pepper your code with assertions, extra tests, and confessions, and generally tear your code apart in whatever way helps hunt down the bug, without fear of disrupting development on the main branch. Then when you’re happy with the bugfix branch, you can merge it back into the main branch (usually called ‘master’ when using git), even if more development has happened (possibly by other developers) in the meantime.
But what if the project has more than one branch under active development? Consider the typical scenario in Figure 1, where in addition to the master branch, there is a stable release branch and a feature devlopment branch. Development on the feature branch is proceeding independently of the master branch. The release branch has diverged a bit, too. (Why? Perhaps bugfixes no longer relevant to the main branch; compatibility patches; etc. It happens.) Should the bugfix branch be merged into these other active branches, as well? Certainly, if the bug affects those branches. But in the process of merging, we should aim to pull in only our bugfix changes – the bugfix should not force us to merge our branches in a more general way at this time. Therefore a bugfix branch emanating from the current master branch (as in Figure 2) presents a problem, because when merged with any other branch, it will pull in all of the current master branch with it. (Basing the bugfix on the tip of any other current branch will suffer the same concerns.)
One solution is to use
git cherry-pick to apply the bugfix selectively to the other branches. This is less straightforward than simple
git merge and has the disadvantage that
git cherry-pick must rename each commit (since the resulting commits have new parents, they must have different names). This makes it less clear that the same bugfix was applied to both branches. Constraining bugfixes to single commits - possibly using
git rebase to squash a branch down – can make
git cherry-pick more straightforward; however, there is still the renaming problem, and I dislike the requirement of one commit per bug; it is reasonable to split up a complex bugfix into steps and to want to retain that information.
The solution I like best involves first finding the commit that introduced the bug, and then branching from there. A command that is invaluable for this is
git blame. It is so useful, I would recommend learning it right after commit, checkout, and merge. The resulting repository now looks like Figure 3, where we have found the source of the bug deep inside the development history.
This approach has several advantages. In a
git log of the bugfix branch, the patch now appears adjacent to the problem it attempts to solve, rather than randomly interspersed with unrelated commits. More importantly, the bugfix branch can now be merged with any appropriate branch using a simple
git merge without dragging in any unrelated commits. The bugfix branch should be merged with any branch that contains the source of the bug (i.e. where we rooted the bugfix branch.) An extremely useful command for this – which I learned here – is
git branch --contains. In our example, the bugfix branch should be merged with master and with the feature branch, but not with the stable release branch, which diverged from master before the bug was introduced.
After the merge (Figure 4), the head commits of the ‘master’ and ‘feature’ branches now have two parents – the previous heads of those branches, and the head of the bugfix branch. Some may recoil at this type of complex ancestry in the master branch, preferring a linear commit history. But why? Handling this sort of complexity is what git is great at.
- Discover bug.
- Identify commit that introduced bug (
git blameis invaluable here.) Can use
git tagto keep track of the commit, if desired.
- Create a bugfix branch rooted at the commit identified in step (2):
git checkout -b my-bugfix-branch <source-of-bug>
- hack hack hack…bug fixed
- Identify branches affected by the bug:
git branch --contains <source-of-bug>
- For each branch identified in (5), merge in the bugfix branch
This approach scales to more complicated repositories having many branches with which you may not be familiar. Steps 5 and 6 can be put into a script that automatically finds all the appropriate branches:
#!/bin/sh # # GCB 1jan12 # Automatically merge $bugfix_branch into all branches containing $bug_root. for b in `git branch --contains $bug_root | sed s/*//`; do echo $b; git checkout $b; git merge $bugfix_branch; done
This real-world example comes from the development of burrow-owl, a package I maintain that draws and manipulates magnetic resonance spectra.
1) Discovery of the bug:
When the documentation-generating program ‘doxygen’ was missing on the system,
make in the burrow-owl doc/ directory would fail (exit with non-zero status),
which is a bug because such problems should be diagnosed during the configuration stage.
2) Uncovering the source of the bug:
Poking around, we find this line in Makefile.am:
and this in configure.ac:
AC_PATH_PROG(DOXYGEN, doxygen, no)
When doxygen is installed, this works as expected. When it is absent, the ‘DOXYGEN’ variable
is set to ‘no’, which ‘make’ then tries to execute – since there is no program called ‘no’,
3) Creating a bugfix branch
We can find the commit responsible for the lines in question using
$ git blame configure.ac 8771b190 (Greg Benison 2008-01-09 11:52:51 -0800 16) AC_CHECK_PROGS(INDENT, indent, [# ** indent not available **]) 8771b190 (Greg Benison 2008-01-09 11:52:51 -0800 17) 2a258920 (Greg Benison 2008-11-23 23:49:24 -0800 18) AC_PATH_PROG(DOXYGEN, doxygen, no) 2a258920 (Greg Benison 2008-11-23 23:49:24 -0800 19) 1288c02a (greg 2009-12-21 07:35:38 -0800 20) PKG_CHECK_MODULES(GLIB, [glib-2.0 gobject-2.0])
It’s apparent that the line we want to change was introduced by commit 2a258,
quite far back in the history of ‘master’. Now we look at that commit in more detail:
$ git log 2a258 -n1 commit 2a2589204454fb5f340a47ba40859f5c3357560b Author: Greg Benison <firstname.lastname@example.org> Date: Sun Nov 23 23:49:24 2008 -0800 added doxygen config file and some inline comments
Earlier commits do not use ‘doxygen’ and do not have the bug, so we create a tag at this commit
which will serve as the root of our bugfix branch:
$ git tag 2a258 bug-doxygen-missing $ git checkout -b bug-doxygen-missing tags/bug-doxygen-missing
4) Creating the patch
Now on our bugfix branch, we make our changes, adding an option to ‘configure’ to either disable
doxygen or specify an alternate program, and if disabled to have ‘make’ skip building the documentation
rather than fail.
$ git log heads/bug-doxygen-missing --oneline b276d6e Check during configuration for missing doxygen f7cf4d8 Updated README.branch to contain description of the problem. 2a25892 added doxygen config file and some inline comments
Observations: Our bugfix branch consists of two commits, then the next parent is
the commit that introduced the bug rather than some other random piece of commit
5) Merging the bugfix branch
The repo contains a ‘master’ branch, where development is occuring, and two
$ git branch
So to which branches should we apply our bugfix branch? Only to those that
actually contain the bug, i.e. that include the commit that began our bugfix
branch. We can find these with
git branch --contains:
$ git branch --contains tags/bug-doxygen-missing 1.5 * bug-doxygen-missing master
Apparently, the bug was introduced before release 1.5, but after release 1.4. So it is
appropriate to merge the bugfix branch into master and 1.5 only.
On master, we can verify that our bugfix branch has not yet been merged, and then merge it:
$ git branch 1.4 1.5 bug-doxygen-missing * master $ git branch --no-merged 1.4 1.5 bug-doxygen-missing $ git merge heads/bug-doxygen-missing
Similarly for branch ‘1.5’; then it would be safe but not necessary
to delete the bug-doxygen-missing branch because it has been merged
everywhere it needs to be.