Before I dive into my main arguments, I do believe feature branches make sense for open source development. Generally, developers will work on branches dedicated to given features. This way, when they are done, they can present the branches to the community, get feedback, and make changes. Often times, they will not even have commit rights, so a committer will have to take the branch and merge it in. This is much harder if the work was a collection of commits and not a cohesive feature branch. It is also common in open source for features to get rejected, so it’s helpful if these commits don’t clutter up the git history. Open source projects also tend to have more sporadic development as opposed to lots of developers all working in the code full time.
The problem is that most work done at companies do not have the same flow. For example, at Braintree Payments, we have about six developer pairs all working on different stories, all committing at the same time, and all with commit rights to our project. We trust that if a pair wrote the code, it’s good and is going into the project. In 99% of cases, if we are working on a feature, it is going to get released.
Given this kind of working environment, here is why I do not like feature branches:
Feature branches make for nasty commit history
Feature branching as a practice encourages lots of small commits, often before tests are passing or the code is in a useable state. Since these commits are on a branch, most people think it’s ok as long as the final merge commit back into master passes all of the tests.
The problem with this practice is that the commit history of the project is now full of broken work and non-passing code. I often use git history to get an idea of why a feature was done, or what other code was changed at the same time. I will “git blame” a method name to see when it was added, and what the code looked like at that point. With feature branches, I can no longer trust that commit. It may have been done on a branch, it may have broken half of the tests, and it may have been completely scrapped before the code went into master.
Furthermore, If feature branches are deleted once they are merged into master, where do bug fixes go? On a new feature branch or on master? There will no longer be an easy way to find a list of the commits that went into a feature. If feature branches are not deleted right away, the repository will likely end up with a huge number of branches.
Feature branches generally have no builds
We use Jenkins to continually test our application. We run numerous builds on every commit, including integration builds that call out to external dependencies.
When we do work on feature branches, we no longer have these builds. Developers will run the main suite of tests before they check in, but no one is going to run the full suite of integration tests on every commit. Furthermore, setting up new builds can be annoying, so we’re unlikely to do it unless we are going to maintain a branch for a long time.
Lots of branches become cumbersome
The lack of builds is one aspect of where lots of branches become cumbersome. Another is user testing. It is common to want the product owner or someone else to take a look at the feature before it’s done to get feedback. If you have a testing environment, they might want to deploy your code there to take a look (rather than looking over your shoulder on the workstation). But if you have feature branches, then they have to deploy your branch, which means they can no longer test other features at the same time. If they are in the middle of testing another feature that requires specific data, they may not be able to deploy your branch (e.g., if it has different database migrations that would require the database to be rebuilt).
Feature branches make refactoring harder
Modern version control tools like git make branching and merging much easier, but they are far from perfect. If we do large refactorings (especially refactorings that take numerous commits), it can become really hard to merge in feature branches. For example, if we rename methods, shift code between classes, or even reorder the methods in a class, the feature branch will likely get tons of conflicts on the merge. Then, someone has to go through each conflict and figure out where the code went. The longer the feature branch lives, the worse the merge becomes.
To take a specific example, say we want to switch our tests from one factory pattern to another. With feature branching, we would create a branch, and spend a few days porting over our tests. Then, when we merge back, we are going to run into conflicts with any tests that were changed by other feature branches. Even worse, while we are porting tests, other developers are still writing tests on their feature branches in the old style.
If we decide to make the test changes on master, but still have feature branches, all of the currently running feature branches have to continually merge from master in order to get the new tests. If they forget, and start modifying tests that have not been ported yet, they are still going to get conflicts when they merge back.
In contrast, if everyone is working in master, the people doing the refactoring can pull, make the changes, and commit frequently. As long as other developers also pull frequently (since everyone is making small, frequent commits), they automatically get the changes.
Given that I don’t like feature branches, here is what I prefer:
All developers work in the master branch. When we are ready to release code to production, we merge master into a release branch, which gets some extra regression testing. We then tag from release and deploy the tag.
We use descriptive commit messages to signify what feature we are working on. For example, we use messages like “#card – message.” If we need to see all of the commits for a given feature, we can grep the git history for the card number. Likewise, if “git blame” a line of code, you can get back to the original story card and see why the feature was developed.
Commits only get pushed if they pass all of the tests. If a developer wants to make temp commits, that’s fine as long as they get squashed into a decent commit before pushing. Spike branches are fine for playing around, but the real work should be done on master once the approach is finalized.
On my last few projects, we have used a tool called rake_commit to do much of this work for us. It will prompt for a pair, story, and commit message, squash unpushed changes, run “git pull –rebase” (if working in git), run tests, and then push if the tests pass. This tool ensures a clean, linear git history.
For long running features, I prefer Branch by Abstraction and feature switches. This way, the features can be developed in master along side other development with the full suite of builds, but not necessarily impact the end user until the work is complete. If we have to release early for some reason (feature X just has to go out today), then we can release without the new features in use.