Git Feature Branching On Steroids
An easy workflow for productive applications
We’ve all been there in the beginning, we learn the basics about Git and start using it for our own projects, university’s coding courses or even in our first job in IT.
Since most of this kind of work only involves ourselves or a few people, we tend to use it as a backup tool or a sort of organizer to track our progress. Sometimes only using the master branch, having no code review whatsoever, and never really having to deal with huge merge conflicts.
That is until we’re working with the real stuff, where you don’t want to mess up a stable product and everybody is trying to publish new features at the same time.
I’ve read quite a few articles about this particular issue, and there are a number of ‘predefined’ solutions you can find, yet none seemed to fully grasp the issues I’ve had while working on big projects.
In this article, we’ll go through how a feature branch approach taken to the extreme worked for my team at Wolox, while developing a huge marketplace app involving 6 APIs in a microservice architecture, a frontend, and a proxy.
The problem that set everything off
Before going straight to the solution, I would like to show one brief example of an issue that we had in the early stages of development that triggered the need for a change in our Git workflow.
We had an integration with an external service that was key to the whole application. One day, that service added new functionality for us to use, and deployed it in their testing environment. At that point, everyone on our team was branching from a development branch, coding the particular feature (in this case, the updated integration), testing it locally and merging them via a pull request to development. Our productive version at that time was a ‘stable’ version of that branch, that we merged to master and deployed at a specific moment, like the end of the sprint.
The issue was that their new service had lots of bugs and was not ready to be deployed to production in time, so we were left with an unpublishable version of our app in the development branch. And the worst part was that every branch made after that merge had that integration code, which we should not deploy. You would laugh at the number of times we’ve had to revert and re-revert that specific part of the code.
Another big problem was that we had to stop merging new code to development about a day or so before the deploy to production, to avoid sending some buggy untested feature to our users.
That’s when we had to think of a better way to use Git.
When to consider this approach
First of all, we have to know when we need to consider this branching strategy for our project. Here’s a list of situations that can happen during development. If we’re facing a few of them, then we most likely use this approach.
- There’s a productive version of our code, with people using our product, and we need to keep adding features.
- We have multiple environments available for our code to be deployed (for internal testing, external testing/staging, live app, beta testing, etc)
- We’re working with 4+ people in the same repository.
- There is at least one integration with an external service that is also in constant development
- We need to be able to deploy bug fixes or features to production at any time, independently of any large code refactor or big feature that we are simultaneously doing.
All right, let’s see what we can do in one of these scenarios.
Save the environments
I’m going to assume that we have a stable version of our code in a branch (let’s say it’s master) and it’s deployed somewhere A.K.A someone is using it (a friend, your coworkers, a million worldwide users, this could apply to any case). I’ll also assume that we have a test and a development environment, just to be complete. The idea is the same and you could downscale to two environments easily, I just want to show you where this model really shines.
We have to start with the concept that an environment is represented by one Git branch, and it should stay like that the whole time. For example, our productive version could be represented by the master branch, we could have a test environment represented by a test branch, where a QA team (if you’re lucky to have one) tests your new features, and finally we could have a more ‘unstable’ environment associated with a development branch. The last one is very useful when we need to try out big refactors, risky integrations, etc. in an environment other than localhost and we don’t want to jeopardize the QA process until we’re sure it’s ok to test.
Once we’re good with this and have everything set up correctly… what do we do when we want to start coding a new feature or fix that annoying bug?
The path to master(y)
We start by creating a branch from master, which is guaranteed to be a stable version of the code. Then, we add the required functionality and test it out locally. The only exception to branching from master is when we need to use code that is in a feature branch that hasn’t been merged to master yet. But don’t worry, in that case, we simply branch from that specific feature, the rest is the same.
Once we feel comfortable to make a pull request to development, we do so in a particular way.
First, we create an auxiliary branch from development named aux-dev/our-branch-name. Then, we just merge our branch to it and create the PR of our auxiliary branch.
Explained like that it seems kind of unnecessary so, why do all of this?
Simple, since we’re working in a very active environment, where PRs are queueing up every day, Git conflicts are bound to happen quite often.
So let’s say we wanted to just merge our branch with the development branch and there are conflicts. Obviously, we can’t merge development to our branch and solve it there, since it will bring code that is not yet ‘stable’. We want to keep the code in our feature branch clean of all other features or bug fixes that have not yet been tested and deployed to production.
Another option is just making the PR to development as is, get a positive code review and do the conflict solving afterward directly in the development branch, with no one to validate that we did it correctly. We really didn’t want to do that, and it also meant that we needed to leave the development branch unprotected for anyone to push code without a code review since that was the proposed way for managing the conflicts.
Finally, the idea of the auxiliary branches came through and it was just what we needed. If there are conflicts, we solve them in the corresponding auxiliary branch and make the PR from it. Everyone could see how they were solved when making the review, all of our continuous integration tests ran smoothly with them, and we could keep the main branches well protected.
The drawback of this system is the little annoyance of creating the auxiliary branches and keeping track of them. For example, if you want to fix something after a review, you need to repeat the process again, fixing it in the feature branch and the merging it to the auxiliary one.
However, this is a minor thing when you have a working production system that you don’t want to crash because there was a misstep in the review process. And by the way, you could write scripts that make the management of the auxiliary branches almost automatic ;)
So that’s pretty much it for the development branch, and the rest of the workflow is very similar. The method for merging into the test branch is the same, we put the development PR link in the test PR description, to make it easier for reviewers to know if there are any differences between the merging of both environments.
In the end, when our code was tested and approved in the testing environment, we can make our PR the master. This time, there are no auxiliary branches to take care of, we just make it directly from our feature branch to master. One thing to keep in mind is that we can always merge the latest changes in master to our branches. That is very useful, if not mandatory when we are working in a long refactor or a big feature that takes weeks to be deployed. It saves a lot of headaches, especially if you’re messing with the core of your code. You don’t want to finally get to make a PR after days of hard work just to find out that you have to rethink everything because someone else added a feature that broke your code a few weeks ago.
Once our code is in master that’s it, we made it! Notice how a problem like the one I’ve explained at the beginning is not even a minor issue with this workflow, and we can have a nice and clean QA testing mechanism.
After that, we make sure to erase our feature branch, the auxiliary ones and have a celebratory drink! Oh wait, there’s another feature to code, guess it’s time to start over again…
A side benefit of this approach is that we’re using only the basic tools for everything Git has to offer us. We’re not using any fancy Git commands, we’re just branching, committing and merging. We don’t have to rebase, cherry pick, squash or anything like that for this to work smoothly. In fact, I advise against using them in the scenario that I’ve shown. Especially when working with a lot of people with diverse experience in the field, some nasty stuff can happen when you let them play with the Git history.
Sure, I love to rebase and keep a beautiful single line in my master branch history, but I’ll save it for smaller projects, where I know everyone is on the same page.
That I believe is the most important matter, knowing WHEN to use a particular Git branching method. This is just one approach, it might work for you, it might not. Perhaps it won’t work for us in the future, and it is for that reason that we should always be ready to question everything. Even ourselves and what we thought was right.