Branch Per Feature in practice
Branch Per Feature is one of the bajillion branching strategies that can be used with modern Version Control Systems (VCSes). I just rotated off a project where I used it for the better part of a year, and even wrote a bit of tooling around it. Here we’ll take a look at BPF in practice, and how it holds up to the theory behind it.
How we got here: a brief history of version control
We begin with a story. (Or, if you’re an impatient git and know it already, skip it.)
In the dark times, keeping track of different versions of your code was hard work because there were no tools to help you. One day, someone grew tired of having their project directories look like this:
my_code.c
my_code.working.c
my_code.old.c
my_code.older.c
my_code.mad_hacks_while_drunk_last_night.c
Since programmers like to answer programming problems with more programming, they wrote some code to track their code, and the first generation of Version Control Systems were born. These systems were better than having to do things manually using backup copies and increasing amounts of your sanity, but there were still problems. You see, sometimes programmers work together on the same project, and sometimes they even work together on the same file in the same project. And in these first VCS tools, there was no way to do so. When you worked on a file, you had to “lock” it so that nobody else could also work on that file, even if you were working on different parts.
And so the second generation of VCSes were born, the most famous example being Subversion. Most of these tools allowed allowed merging. Multiple people could now work on a file at the same time. The first person to submit their changes to the system did so normally, then subsequent developers then had to merge their changes into the first developer’s version. If the developers changed different lines in the file, this could be done automatically, but if multiple developers changed the same lines, they had to merge them by hand. And for a while everyone (or almost everyone) was happy, but there were still problems. You see, these systems had a client-server architecture. If you wanted to do anything, like commit some changes or look at the history of changes, you had to connect to a server. This made things kinda slow, especially as the repository (the big VCS word for “project tracked by VCS”) got larger and the history became longer, giving you more to have to transfer across the network. It also encouraged you to wait to commit your changes to the repo until they were just right, since once they got pushed to the server, everyone got to see them. And programming without an internet connection was difficult, unless you wanted to pile up your changes and merge them the next time you got the chance. This could be… onerous.
And so the third and current generation of VCSes were born, including examples like Git and Mercurial. These VCSes are distributed. Everyone keeps a copy of the whole repo, including all the history of all the changes ever made. This means you have to store a little more on your hard drive, but now doing just about anything is a matter of reading some files. Sharing your changes with other developers now becomes a matter of syncing your version of the repo with theirs, which is still usually done through a central server. There are still a few problems, but for the most part all is well. This brings us to…
Branching
Originally the history of a VCS repository looked something like this:
It’s simple and it works. But sometimes it’s useful to branch your code into separate histories:
Say you want to create a branch for a specific version you released to customers.
Fixes are implemented on that branch,
while at the same time continued (experimental) development continues on the “trunk”.
Fun fact: this is where the name for the Source game engine
comes from — the old code was put in a branch named GoldSrc
,
the new stuff went into a branch named Src
, and the rest was history.
In the old days, branching was a bit of a pain, and merging branches together was even worse, so people usually only did it when really necessary. But with distributed VCSes, easy branching and merging became a must, given that your local copy is a “branch” that needs to be merged with another developer’s every time you sync repositories.
So now branching is easy. In Git, a branch is literally just a file with the name of a change in the repo history. Since it is so trivial, people use branches for all sorts of things. Contributing to open-source projects is easier than it ever has been. You make a personal branch of the project, make the changes you want, then ask the leader(s) nicely to merge your branch with a “pull request”. Branches also make it simple to experiment with a feature or set of features without worrying about breaking things or changing things from under other developers’ feet.
Many different projects have many different approaches to branching. Some just maintain one “master” branch and use pull requests to discuss and bring in changes. Others use Git Flow. Yet others use Branch Per Feature.
What is Branch Per Feature?
Proposed three years ago, almost to the day, by Adam Dymitruk, Branch Per Feature (BPF) works on the basic premise that each feature or piece of work gets its own branch. Other branching strategies, at the end of the day, work by merging new work into a “master” or “mainline” branch. However, if one of these merges breaks something, you have to take time to back it out, especially if you’ve done other merges since then. In BPF, you test your branches by merging them to a throwaway development branch. Once you’re happy with how they interact with the other features there, you send them off for actual testing on the release candidate branch. Because each feature is on its own branch, building a release candidate is as simple as merging whatever features you want. Once a release is put out the door, the development branch is reset to that latest release.
When it works, it works great!
When everything goes according to plan, BPF is great. Features are nicely decoupled, which makes testing and releases a breeze. Branches which don’t make the cut for a release can just be thrown out until next time.
But sharing is hard.
Unfortunately, BPF is harder than it initially seems. When developers merge their work into the development branch, we want to remember how any merge conflicts were resolved so that we don’t have to go through the same pain later when we merge work into the rc branch to build a release candidate. Git allows us to do this with the rerere feature, which stands for “reuse recorded resolution”, and stores merge resolutions in the Git metadata directory. However, we need to share these resolutions among the team, and many do so by keeping track of the resolution cache in a separate branch of the repository.
This means that each developer on the team now has a Git repository in their Git repository, and for everything to work nicely, you have to keep both of them in sync.
You’re a programmer, so it’s easy enough to write some scripts to help, but it’s a hard task to get right.
Isolation is hard.
Keeping branches independent is also difficult. BPF works best when features have no dependencies on each other since this lets them be added or removed from a release at will without affecting any other features. Merging feature branches together defeats the purpose of BPF. Unfortunately, it can be hard to keep features separated. If, for example, you write some new code or do some refactoring for one feature, and that code can be shared with second feature, you have a dependency. You could argue that these two items should then be considered a single “feature”, but it’s quite possible that doing so would then make that one feature far too broad.
There are a few ways to address this, but none of them are ideal.
-
You can cherry-pick commits from one branch to the other, but this only masks the dependency – the code from one branch still depends on code from the other.
-
You can merge the branches, but this defeats the purpose of BPF, and in doing so adds complications to testing since one branch is now essentially an alias of the other.
-
You could also combine the two features into a single branch, but since BPF tooling (at least the stuff I’ve worked with) assumes a one to one relationship between a branch and a feature or story in the issue tracker (assuming you are using one), this requires some manual fiddling and communication with the test team.
Is it worth it?
I can imagine BPF working really well for small bug fixes and feature requests on a large, mature code base because in these cases it’s fairly easy to avoid dependencies between features and their branches. But anything that is still growing fairly rapidly or undergoing lots of refactoring becomes a quickly-moving target. BPF also introduces a lot of complexity into your workflow. As we just saw, you have to handle a second nested repository and keep both synced, or develop non-trivial tooling to help you do so. The very nature of BPF also means that there’s many branches in-flight between releases.
Also concerning to me is that hard resets and forced Git pushes are part of the normal release cycle in BPF. Git users are usually taught that these are tools of last resort, because rewriting history means that everyone in your team must manually reset their branches. Here is another place tooling can help, but is all this additional complexity worth the purported gains of BPF?
I am inclined to say no. Programming projects are complex because the world has complex problems, but you should strive to keep your tooling simple. Simpler tooling usually means less meta-work and more time to spend writing the code you actually need to write. In my eyes, BPF doesn’t give me enough benefits to be worth the trade-off.