In this article we’ll explore the use of feature branches based off of GitWorkflow to integrate features and fixes only when they are fully ready to go. While this is a less well-known workflow than others, it offers a significant degree of freedom and flexibility.
We won’t be covering all of GitWorkflow in this article, but rather a simplified variant focused on working with a single persistent branch, a single integration branch, and individual feature branches.
Note: To those familiar with GitFlow, I should note that although the names are similar, these are two entirely different workflows. This article will only cover the Git Core Team’s Workflow. If you’d like to compare and contrast, I recommend this HackerNoon article.
The Need for a Dynamic Git Strategy
Before I describe what this workflow is, let’s look at a few scenarios:
- Imagine you’re responsible for an application inside of a larger organization. You add features to it and provide fixes to issues as they come up. Your organization likes to release frequently and you may or may not know which work items will be included until near the final pre-release testing cycle.
- Alternatively, you’re working on the same sort of application and one of your features has an obscure bug found late in testing and needs to be pulled from the release.
- Or imagine a developer implements a new feature which brings a testing environment to a halt and must be removed until remedied so that testing can resume.
In any of these scenarios, a certain degree of development agility is needed. Your core need is to be able to quickly remove features or fixes from a release without introducing major risks.
This is what the workflow is all about – being able to quickly add or remove features to a branch as needed.
What about just using a Development Branch?
So, why is this needed? Can’t we just have all developers code on a persistent development branch and periodically split off branches for major features, then merge them back in?
Well, maybe, but if you discover an issue with a two week old feature commit in your development branch just before release, what are your options?
- Delay the release to get a fix in
- Make a new commit to disable the feature, hoping that you disabled it correctly and that it was the only new issue introduced
- Release with the defect
In my opinion, none of these are fantastic options. You’re forced to take unnecessary risks or push a date back when you might not need to.
Branch Structure Overview
With the Git Core Team’s workflow, you just custom order a release based on a combination of branches that doesn’t include the one that introduced the issue.
Under this workflow you have feature branches which represent individual work items being changed. These are your individual fixes and new features.
Each feature branch branches off of the master branch. This is nothing too unusual except what it represents. Master represents releasable code. This is code that is fully tested, reviewed by product management, and does not introduce any regression issues.
Where the workflow gets interesting is with it’s concept of a proposed updates branch (often referred to as pu for short). Proposed updates is a branch containing finished features or fixes that are ready to be evaluated in concert to form a finished build.
Note: GitWorkflows also advocates for a
next branch and a
next equates to a pre-release environment while
maint is based off of the last production release. You may or may not need these branches.
Testing and Integration
So, all testing and product review occurs based off of the proposed updates branch. Once that integration testing branch is certified good, the feature branches for individual changes are merged into master after rebasing the feature branch onto master prior to merging them in.
This does a couple of things for us:
- It keeps version history nice and clean
- It makes sure that feature branches don’t seep into each other so you’re not dependent on code you don’t mean to be
- If the integration branch gets corrupted, we can regenerate it at will
I could explain these more in detail here, but it’s best to show you the life of a single feature.
Case Study: Adding a Dark Theme
Because every application needs a dark theme, let’s use that as an example in our case study.
Priya looks at the team’s agile board and decides to take on the “Add a Dark Theme” story with an ID of “FOO-123” in the ticketing system. She looks over the user story and acceptance criteria, decides she has enough information to get started, so she assigns it to herself and creates a branch.
She creates this branch by switching to the
master branch and then running
git checkout -b FOO-123
This will create a branch based on the work item’s identifier (FOO-123) off of the current branch and then switch to it.
Priya then spends her time testing and making changes until she’s satisfied with her feature. Once she’s ready, she opens a merge request (AKA a pull request) from
FOO-123 to the
pu (proposed updates) branch.
Jerome does a code review on the branch and is happy with the work and approves the merge request so Priya merges it into
The continuous integration server sees the change to the
pu branch which triggers a build. The build succeeds and is later deployed to a test environment.
From there, quality assurance reviews the feature and finds no issues. Product management looks at it and is likewise pleased.
Removing items from Proposed Updates
Unfortunately, Landon has been working on a new page that didn’t take the newly-introduced theme into account. The dark theme doesn’t render well on this page and so when Landon’s page is reviewed by product management and quality assurance, an issue is created.
Because there’s only one day left in the development cycle and Landon’s new page is critical to the business, the team decides to remove the dark theme from this release and move it into the next one.
In order to do this, the team decides to recreate the
pu branch off of the current
master branch. They do this either by deleting and then re-branching
pu off of the latest
master commit or by hard resetting
pu to match the latest
master commit and then force pushing.
Note: Force push is dangerous and destructive and can blow away changes on the branch. In this case, we want to remove changes to the
pu branch and start with a pristine state. If having a few qualified people perform a force push to reset the
pu branch periodically is a turnoff for you, I recommend looking at deleting the
pu branch and re-branching instead, but it may be additional overhead to do so.
pu branch is available, all feature branches that should be integrated into it are merged back into it and a new build is generated and deployed to the testing environment.
I like to think of this as a highly configurable menu where you can choose whether or not each feature is part of a branch.
Integrating into Master
Next sprint, once the release has cleared, Priya makes some additional tweaks to FOO-123 to allow the dark theme to render properly on Landon’s new feature. Once she is ready, she submits another pull request and then merges into
pu once it is approved.
This time everything works well and FOO-123 is approved and cleared into the release. The feature now needs to be merged into the
pu directly into the
master branch can work, it has a few downsides:
- You need to check the
pubranch to make sure that every feature integrated with it has also passed testing and is approved for the release. If you make a mistake, you added something untested to a production-ready branch.
- Merging an entire integration branch into
mastermakes it a lot harder to read and understand the individual commit history because features move from one branch and into another. While it may make sense looking at a single feature, it’s harder to read the actual overall branch history.
Because of this, we need to merge the feature branch into master instead of merging proposed updates into master.
This is the number one issue people have with understanding the Git Core team’s workflow, so it’s important to stress.
Instead, we first switch to our feature branch and then
git rebase master to rebase that feature onto the latest version of master. This helps keep our version history pristine and allows us to resolve merge conflicts inside of the branch instead of inside of the branch we’re integrating with.
Once that’s done, and the merge is committed, we switch to the
master branch and then merge the topic branch in via
git merge FOO-123 (the name of the branch).
With the merge committed and pushed, it is now a formal part of the next release and the individual feature branch can be deleted if you so choose.
What if master has a problem?
If code gets into the
master branch that needs to be removed, you now have a problem. This workflow isn’t great at handling this task because
master is supposed to be persistent.
Because of this, I highly advise you to delay merging into
master if you have any doubts since it’s a lot easier to merge into master later in the process instead of trying to remove something from or patch master.
One of the things you can do is institute a rule where once work items are approved by product management, they are closed and merged into
master and any adjustments will need a new work item instead of reopening the old one. In my experience, this tends to produce good results, but your mileage may vary.
If you do need to make adjustments to
master you have a few options as I see it:
- Require a new work item to go through the pipeline and enter the
masterbranch with whatever change you desire.
masterto a known good state, then force push and merge items past that state back in.
- Use a revert commit to revert a specific commit to master and reintegrate later.
None of these are great options:
- You always want
masterto be releasable, so waiting for a new item to come through limits your agility.
- Force push is something too dangerous to be doing on your
masterbranch except in extreme circumstances, so it shouldn’t be part of your normal workflow.
- Revert commits muddy up your version history and may not even work if subsequent commits modified the same areas of code.
All in all, you’re best off spending the extra time to make sure that features are really good to go before integrating.
I strongly recommend that you tag releases on the
master branch as the existence of a tag allows you to quickly create a
maint branch for offering hotfixes as needed later on without having to guess or search for which commit hash corresponded to your last production release.
I advocate that you should reset your
pu branch to
master at the beginning of every sprint. This helps keep branch comparisons accurate when comparing a feature branch to the
pu branch. This also makes it easy to identify what’s in the
pu branch and still up in the air.
Some version control tools such as GitHub’s web user interface handle merge conflicts by merging the branch you’re trying to integrate with into your feature branch. This is a problem when trying to merge a feature branch into
pu since you never want other features to enter feature branches without your knowledge. Instead, use command line or an external tool such as GitKraken to resolve merge conflicts in
If you don’t like the name
pu, I recommend calling the branch
qa. I personally go with
releases instead of
qa instead of
pu, but the concepts are the same, regardless of which terminology you use.
What about Maintenance Releases?
This model is not well suited for long-running maintenance branches. If you are working in an infrequent release environment, you should consider something like GitFlow or another git methodology.
In general, under the Git Core Team’s workflow, you’ll find yourself wanting to release more frequently from
master as opposed to providing hotfixes into production.
However, if you do need to do a hotfix, you can do it by switching to the tag you created on the
master branch for a given release and creating a new branch at that location representing the production patch.
Alternatively, you could always have a
maint branch and update that branch to point to the latest production release commit every release.
Once your hotfix branch is created, you can branch off of this branch to create an individual feature branch for your production patch. You’ll then merge your feature branch into the hotfix branch, generate a build, get that build verified by QA and product, and then apply it to production.
This different workflow exists to keep changes in
master more recent than the production tag from reaching production.
Once a production patch is applied, the feature should also be merged into
pu and then
master once tested in
pu. Failing to do this will result in a regression issue where the applied production patch is no longer present in the next release.
While you can handle maintenance issues this way, if you do have a habitual need for production patches and maintenance branches, you should consider releasing more frequently so that defect resolution can be more easily achieved in planned releases and patches are reserved for those truly horrific high severity bugs.
You should also take a good look at your development and testing practices if things severe enough to warrant production patches are a regular occurrence for your team.
This workflow isn’t for everyone, but if you want absolute control over what features are in which builds, this is a workflow to strongly consider.
This is only one flavor of the GitWorkflow. If you’d like to learn more, I recommend reading the following materials: