Many software development teams at one point or another look for that perfect branching strategy, one that would make release management straightforward, while keeping their source repository manageable. More often than not many end up trying heavily promoted Gitflow workflow, without realizing that Gitflow doesn't work for products with more than one actively supported release.
On the surface Gitflow does sound reasonable. Have a look at Attlasian's Gitflow Workflow tutorial on Bitbucket:
Ignore green feature branches on the bottom of the diagram for now because they come and go throughout the development cycle and have no bearing on release management. It's also not clear why Atlassian would not use semantic versioning for this tutorial and instead used versions v0.1, v0.2 and v1.0. In order to make things easier to compare, here is the same diagram, but without feature branches and with semantic versions.
Yellow boxes on the diagram represent tags, green commits represent release branches, which are created for a specific product release. These branches receive only changes intended for the immediate upcoming release. While a release is being prepared, new development continues on the dev branch without affecting the release. After the release is published, the release branch is merged into master and also into dev, so the former contains only released code and the latter receives all release work. The release is tagged as v2.0.0 only in master.
The process repeats towards the next release, which is v2.1.0 on the diagram. If a bug is found before v2.1.0 is released, a hotfix branch is created off master and is later merged back into master and tagged as a hotfix release v2.0.1. Hotfix changes are also merged into dev for the next release v2.1.0.
Now consider this workflow for a non-website type of a product, where a bug may be reported against one of the past, but still supported product versions. Specifically, consider that after v2.1.0 has been released, a bug is reported for v2.0.1. The user is not yet prepared to take the latest and greatest v2.1.0 and would like just that one bug addressed in a hotfix that would have been v2.0.2, but has no place to go to because v2.1.0 now is in the way.
Gitflow doesn't account for this scenario and people get quite inventive trying to solve this problem and usually end up creating a new branch off v2.0.1 to release v2.0.2 off that branch. This messy process continues and after a while the repository becomes a mess of adhoc branches and spotty tagging, and the clean diagram above becomes nothing but a distant memory.
Keeping it Simple
Let's refactor the diagram above to make it easier to see how multiple active releases can be maintained.
Notice that master in the original diagram seems to serve very little purpose. The original post promoting Gitflow describes this use of master as a way to make a build off master whenever it receives a new commit, which is possible because master is considered always production-ready and any commit to master is expected to produce a robust build because all testing was done on the release branch. While this is a nice sentiment, this practice of using Git as a build management substitute should be avoided in favor of better ways to manage builds and build artifacts.
Without the master branch (just for discussion purposes), tags and release branches that serve the same original purpose would look like this.
Now it is easy to see that in order to release v2.0.2, all we need to do is to continue the release branch. However, we cannot quite keep repeating the hotfix commit pattern because the merge into master cannot be done after a release branch for v2.1.0 is created simply because the merge would have nowhere to go. This is where dedicated release branches come in.
Dedicated Release Branches
Let's remove those merge commits for v2.0.0 and v2.0.1 tags, which brings us to a workable branching strategy shown below. This strategy maintains each release branch for as long as necessary to support that version of the product.
In order to support parallel development, a release branch is created when all features for the immediate upcoming release have been decided upon, so development for v2.1.0 can continue on master while v2.0.0 is being finalized on the release branch 2-0-x.
This approach keeps release branches in a well-structured repeatable branch topology and presents a very clean view on what fixes each branch contains. However, it is not without a flaw.
Notice that the release branch never merges into master, which means that changes intended for both, v2.0.0 and v2.1.0, must be grafted (cherry-picked) between 2-0-x and master. While grafting can be easily done in most cases, even for multiple commits, the biggest problem here is that fix A in the diagram above would appear in two dot-zero releases - v2.0.0 and v2.1.0. This requires a process set up, manual or automated, that removes such fixes from the release notes of the newer release, which is an extra release step to maintain, and may get quite messy.
Another negative side effect of this branching strategy is that QA has to verify fix A in the release branch and in master, which means it has to be erroneously planned for releases v2.0.0 and v2.1.0 in the issue tracking system, so QA knows where to test this change, but it will be eventually removed from v2.1.0 release notes, which also messes up issue tracking and creates confusion about where the change was really released.
Dot-zero releases are those with the last component of their version being zero, such as v2.0.0, v2.1.0, v3.0.0, etc. Dot-zero releases are perceived sequential and the same fix should not appear in release notes of more than one dot-zero release.
This blog post doesn't cover more complex multi-level branching strategies that would allow v2.2.0 being worked on after v3.0.0 has been released, which may not necessarily be considered sequential despite the last version component being zero in both of them.
Any product release following a dot-zero release lives its own life and will receive its own fixes that may appear in other product releases as well. In other words, it is fully expected that v2.0.1 and v2.1.3 could contain the same fix in their release notes for users who could not upgrade to v3.0.0, which may also list the same or a better fix for the same issue.
Development can continue on master until changes intended for the immediate upcoming dot-zero release and for a future dot-zero release need to diverge. When this happens, changes intended for future releases need either to wait for the immediate upcoming release to be finalized or to go on a separate branch. This brings us to two different branching strategies - one is good for smaller teams and projects and one is for more diverse ones.
Delayed Release Branches
Teams that tend to focus on the upcoming release and don't have much work being done towards the next release following the immediate one would postpone creating a release branch until a dot-zero release is made off master. Any development work that is not going to be released in v2.0.0 can live in feature branches, shown as gray commits below, or simply postponed in patches, or stashes, while the upcoming dot-zero release is being finalized on master.
Change A in this model is committed only on master, which keeps commit A only in release notes for v2.0.0. Changes for releases following a dot-zero release, like change B above, still need to be grafted between branches.
Another less obvious, but important benefit of this approach is that merges from pending feature branches will be done by each developer who is working on each feature and any merge conflicts will be resolved by a person who is familiar with the incoming changes and can resolve potential conflicts with more understanding of what the final code should look like.
All changes that were introduced in 2.1.0 since 2.0.0, including those in gray commits in feature branches, can be listed with the Git command git log 2.0.0..2.1.0. The equivalent Mercurial command is hg log -r "2.1.0 % 2.0.0".
Release Branches for Parallel Development
The second approach is more suitable for larger teams that cannot keep changes for future releases in feature branches because their changes need to be tested together with other pending changes.
Considering that there is always only one upcoming dot-zero release, the tagged release commit, such as v2.0.0 on the diagram below, can be merged back into master while the release branch continues, so it can support any additional patches for v2.0 of the product.
A release branch in this case is created before the dot-zero release, so teams working on features for the future v2.1.0 release, can commit their changes to master, while the dot-zero release team commits their changes to the 2-0-x branch.
Changes intended for v2.0.0 are committed only into the release branch because it will merge back into master after v2.0.0 is released, usually within 2-4 weeks. Change A in this case is committed only to 2-0-x, which also keeps it only in release notes for v2.0.0. However, change B for v2.0.1 is grafted between branches, just like in other cases for releases after any dot-zero release.
From the issue tracking perspective, change A should be verified by QA only on branch 2-0-x and should considered as verified when v2.0.0 is merged back into master simply because released and closed issues cannot be reopened. Any regression resulting from this merge should be tracked with new issues. Change B, on the other hand, should be tracked for releases v2.0.1 and v2.1.0 in the issue tracking system and should be tested in each of the branches before its corresponding issue can be closed as verified.
The downside of this approach, in contrast with the previous simpler branching model, is that one person will have to merge v2.0.0 into master, which means any potential conflicts may need to be resolved by somebody other but the original author. However, given that a dot-zero release is typically being worked on during 2-4 weeks, this merge shouldn't result in too many conflicts.
Similarly to the simpler branching model, git log 2.0.0..2.1.0 and hg log -r "2.1.0 % 2.0.0" will list all changes in 2.1.0 introduced since 2.0.0 for Git and Mercurial, respectively.
Subsequent Release Branch Merges
One urge many will have will be to continue merging the release branch into master after the initial dot-zero release. In the two diagrams above, it means that revision B may be attempted to be merged into master before branch 2-1-x is created instead of grafting it onto master. As tempting as it sounds sometimes, such merge has several undesirable consequences.
It's important to highlight that while a dot-zero release will typically be prepared within 2-4 weeks from the moment a release branch was created, the time between a dot-zero release and any other release on that branch may be weeks, months and even years, so in most cases it won't even be possible to merge because there will be other dot-zero releases in the way, like the v2.1.0 release in the diagrams above.
However, even before a branch for the next dot-zero release is created, a possible merge from v2.0.1 shown on the diagram below is not a good idea.
Such merge breaks continuity between dot-zero releases and will bring into master not only the desired fix B, but also a tactical fix X that was meant only for that specific release and not for the next dot-zero release, where it was removed or reworked as commit X'. Tracking and weeding out such unwanted fixes in the merge will be error-prone and quite laborious for larger projects.
Another way to describe the broken dot-zero release continuity is that the target version for each issue in a bug tracking system, such as Fix Version in Jira, should always have only one dot-zero release, but may have multiple versions for releases following dot-zero ones. For example, change A may appear in v2.0.0, but not in v2.1.0. Change B, on the other hand, should appear in v2.0.1 and 2.1.0 because there is no perceived continuity between v2.0.1 and v2.1.0, but in this topology it would only be listed in v2.0.1 because the issue would have to be closed at the time of that release, which means it cannot be tested in master, as the subsequent merge hasn't happened yet. People sometimes work this around with cloned issues, but it creates all sorts of negative side effects with different issue numbers tracking same underlying issues.
Lastly, the merge from v2.0.1 makes the query git log v2.0.0..v2.1.0 not as useful because it will show commit X that was released in v2.0.1, but was removed or reworked in v2.1.0, so the resulting list cannot be considered final until somebody confirms what's in and what's out, such as commits B and X above.
In fact, this last diagram takes us back to the amended Gitflow diagram with a hotfix commit merged back into master, which is just something to stay away from.
Diagrams in this post are created with app.diagrams.net.