Here are some Git workflow anti-patterns I’ve observed in my career. This is not meant to be an exhaustive list. These are just some of the common ones I’ve come across.
And as a side note: I firmly believe that the workflows I describe in the post are the best way to do things most of the time, but at the same time I know that there’s almost never a right answer that applies to everything all the time. For example, what’s appropriate for a 20-person team isn’t appropriate for a solo developer. Keep that in mind as you’re reading.
Now let’s get into the anti-patterns.
Long-lived feature branches
I don’t like feature branches to last for more than a few days. The longer a branch lives, the more likely it is that there will be certain problems, discussed below.
My feature branches tend to last less than one day because often the entire cycle of coding, review, deployment and verification all happens inside of one day.
Problem: merge conflicts are more likely
The longer a feature branch lives, a) the more code there will be to merge and b) the more opportunity the master branch will have had to get out of sync with the feature branch.
Problem: the PR will be harder to review
This one is kind of self-explanatory. The more stuff there is to review, the harder it is to review it.
Now let’s talk about some of the upstream causes of overly long/large feature branches.
Upstream cause: feature was too big
A very common cause of a branch that’s too big is a feature that was too big. In my experience programmers and team leaders are often bad about letting user stories through that are too big and too nebulously-defined. If there’s not a super clear answer to the question, “How exactly do we know when this feature is done?” then it’s a sign the story isn’t “shovel-ready” and that it should be clarified more before someone starts working on it.
Upstream cause: ignorance of feature flags
Sometimes it seems that a long-lived branch is necessary because the feature is irreducibly large and the feature can’t be released to users until it’s all the way done.
I’ve never seen this problem stand up to an intelligent use of feature flags and a judicious sequencing of the coding work.
In my experience it’s always possible to work in a sequence more or less like this:
- Perform the foundational work for the feature (e.g. creation of new database tables, etc.), then merge and deploy
- Write the model-level code (i.e. not the UI) for the feature, then merge and deploy
- Build the UI for the feature, but put the UI behind a feature flag, then merge and deploy
- Toggle the feature flag “on” to surface the feature to users
It’s true that using feature flags adds overhead. It’s more tedious and time-consuming to use feature flags than to not. But using feature flags is less time-consuming than dealing with all the problems introduced by long-lived feature branches.
Misunderstanding the reasons for merging and deploying
Git workflows and deployment workflows are inextricably linked. The way a team handles deployments has a bearing on how it uses Git and vice versa. Let’s talk about the reasons for merging and deploying.
Deployments aren’t just for shipping code
On the surface, it seems like the reason for deploying is to deliver new value (features and bugfixes) to users. This is true, but all by itself, it’s overly simplistic. There are practical considerations involved as well.
Every deployment carries a risk of introducing a problem. The bigger the deployments, the more stuff there is to potentially go wrong. The less frequently deployments are performed, the bigger the opportunity for someone to forget a crucial step in the deployment workflow, for software rot to cause a problem with the deployment mechanisms, or for some other problem to creep in.
We do deployments to deliver value to users but also to maintain dev/prod parity. Small, frequent deployments make it less likely that any one deployment will be problematic.
Frequent merging is essential to dev/prod parity
I can’t have tight parity between dev and prod if I don’t have tight parity between my master branch and my feature branches. The delta between production and development is “code in development minus code in production”. All the code in my feature branch is “code in development”, so it all counts toward widening dev/prod parity.
To summarize: long-lived feature branches make deployments riskier.
Using a complicated Git workflow like git-flow
I’ve worked at one or two places that used git-flow. I’ve always found it painful due to its needless complexity and its sensitivity to human error.
With git-flow, you have a
develop branch that exists in perpetuity alongside
master. In addition there are potentially some ephemeral release and hotfix branches. All feature branches are branched off of
develop are merged back into
develop. At one place I worked we also had a
staging branch that lived in perpetuity.
Some people can handle all this mental juggling but I can’t. I also don’t see the point since other branching models exist that are both simpler and better.
My preferred branching workflow
I like to use a branching workflow like this:
- Create a feature branch off
- Build the feature
- Create a PR for that branch
- Merge and deploy
- Delete the feature branch
That’s it! No need for a “branching model” that takes a lengthy blog post to explain. Just make a branch, merge the branch, then delete the branch. It’s very similar to GitHub’s Git workflow.
- Don’t take on stories that you think will take more than a few days to build. If a feature seems bigger, break it down into several smaller stories.
- Don’t create branches that last more than a few days. If the feature seems irreducibly large, consider using feature flags and a smart sequencing of the development work to release the feature in smaller chunks.
- Deploy frequently, preferably every time you merge to master. Deployment workflows and Git workflows are deeply linked.
- Use a simple branching workflow like GitHub flow.