Version control is an indispensable tool in modern software development, and Git is one of the most popular and widely used version control systems available today. However, despite its pervasiveness, many development teams do not use Git to its full potential, often due to a lack of a well constructed and adhered-to Git workflow. This can lead to frustration with version control as well as process problems, which are easily solvable by adopting a different workflow strategy.
In this article, I’ll be discussing the differences between a few different high-level Git workflows as well as diving a bit deeper into a particular structured, opinionated Git branching workflow known as Gitflow, which has helped a number of teams at iZotope ensure high quality and build integrity while minimizing common pain points associated with using version control on a team.
Version Control Overview
Historically, most version control systems were based on a centralized topology, in which one remote server acts as the “source of truth” for all consumers of a repository. Under this topology, new commits to the repository are immediately pushed to the server and other clients are forced to update their local working copy and resolve any conflicts before they can commit new changes. This is how Apache Subversion (commonly known as SVN) works, for example.
While it is possible to create branches in centralized version control systems, the relative difficulty of doing so tends to prevent teams from using branches frequently in their VCS workflow. Furthermore, the eventual merge back to the core branch (or
trunk) can be a painful process in a centralized version control system, meaning that having more branches often leads to more time spent resolving VCS issues instead of developing software.
Git, by contrast, is a distributed version control system, meaning that each client has a complete, isolated working copy of the repository, which may be modified in isolation from the remote server and synchronized later. In addition, with Git there can be multiple remote servers hosting the same repository or different versions of the same original repository, commonly known as “forks”. Synchronization between client copies of a Git repository, known as “clones”, and any of potentially several remote servers can be done at will, and per-branch.
Branching and merging are also both significantly easier using Git as compared to centralized version control systems. Clients can make branches and resolve merges locally without updating the remote server, which allows a lot of flexibility in terms of validating merge results, reorganizing/editing commits, or even completely restructuring branches before committing the end result to the remote server.
New Git repositories are created with a single
master branch by default, the purpose of which can be interpreted in a number of ways. The most basic (and, as you will see, least useful) way to use the
master branch is in a similar way to the
trunk in SVN and other centralized version control systems, in which it represents the latest version of the source code, and is the branch to which all commits are made during development.
Single-Branch Git Workflow
A very basic single-branch (
master only) Git workflow typically works something like this:
- Clone the repository
git clone [my_repo_url]
- Change some code
- Add changes to the index
git add [changed_file]
- Make a commit
git commit -m "Changed some code"
- Push the change to the remote server
git push origin master
- If the push fails because the remote server is ahead of the local copy, pull first and fix any conflicts, then push again
git pull --rebase
- Note: The use of
--rebaseis optional here, but it avoids polluting the repository with superfluous merge commits
This type of Git workflow is not much different from centralized version control; there is still only one primary “source of truth” (the
master branch) where all code changes are made by everyone on the team. Despite this, some teams never move past this workflow, which can lead to a number of problems as teams scale up in size and move toward a regular release schedule. These problems may include the following:
Different development arcs are all mixed on the same branch
No matter how your team chooses to break up specific tasks, it’s probably the case that not everyone is working on the same thing at the same time. In a single-branch workflow, commits containing completely unrelated code changes all end up on
master in an unknown state of completeness and stability.
Neither features nor bugfixes can be tested in isolation
If part of your team is working on one particular task, be it a feature or bugfix, it is very difficult for QA to validate new changes, since the state of
master is constantly in flux and potentially consists of commits made as a part of multiple, unrelated tasks. At best, other commits do not affect the change being tested, but in many cases the change may not be verifiable at all due to instabilities introduced from other unrelated changes.
There are no implicit guarantees of branch stability
This one is extremely important: in a single-branch workflow, there are absolutely zero built-in guarantees that the HEAD of
master represents a stable, working build. This can be solved by creating tags for commits representing good builds, but a better solution is to avoid the problem altogether by using a different Git workflow.
“Code Freeze” can be very painful
When it comes time to prepare a build for release, teams using a single-branch workflow have very little flexibility when it comes to what goes into the build or not – once changes are committed to
master, it’s difficult and costly to productivity to revert them if the relevant feature isn’t ready for release yet. This means that your team has to be very careful about commits leading up to a release, which can be especially troublesome for teams trying to adhere to an agile release schedule.
So, if the single-branch workflow has so many issues, what is a viable alternative?
Creating, updating, and merging branches in Git is comparatively easy — in fact, the relative simplicity of its branching model is one of the distinguishing features of Git as a version control system. I won’t go into detail on how branching works in Git, but there are plenty of resources out there if you are interested in learning more about the mechanics.
In a branch-based workflow, developers create “feature” branches from
master or another stable branch to implement feature work, bug fixes, or other maintenance in parallel with other ongoing work. When the work is completed, it can be tested in isolation from other codebase changes by building the relevant branch. Once the branch’s build passes the necessary quality checks, which might include code review, unit tests, and manual or automated regression tests, the branch is then merged back into the stable branch.
Since branching is easy in Git, creating and destroying branches does not impose a significant overhead cost on productivity. In addition, making use of a branch-based workflow solves all of the aforementioned issues with single-branch workflows in an elegant way:
Branches allow features to be developed and tested in isolation
Because each feature or bugfix is implemented on a branch, the changes introduced in builds generated from feature branches are always directly related to the specific feature or bugfix. Therefore, there is high likelihood that any regressions or issues discovered during testing will have been introduced in the course of development of the particular feature or bugfix being tested. This can save a lot of time and effort for both QA engineers and developers in tracking down and fixing bugs.
Branches ensure build stability
In a branching workflow, one or more long-term branches (e.g.
develop, see below) are designated as “good” branches, to which only working, fully tested code is merged. This means that at any point in time, your team can have confidence that the code from one of these branches will produce a stable build.
Branches make “Code Freeze” a breeze!
Preparing a release candidate and integrating stop-ship fixes is extremely easy in a branching workflow. Because there is always a stable branch which consists of up-to-date, fully tested features, all you need to do is create a new branch from the stable branch in order to institute a code freeze. Builds generated from the release candidate branch can then be verified for the release at hand without stopping development on features for future releases.
Gitflow is a specific branching strategy for development teams first proposed by Vincent Driessen in a 2010 blog post. It provides a common, structured template for teams to follow in order to implement a successful Git branching workflow. It’s both easy to learn and highly effective, and it has benefitted a number of teams here at iZotope.
In Gitflow, there are at least two branches that are permanent — that is, they are never deleted for the entire lifetime of the repository.
In Gitflow, the
master branch represents code that is currently in production. That’s right — the
master branch will remain empty until you have released the first version of your product to the public. The advantage of keeping only in-production code in the
master branch is that it’s very easy to find the code that’s out in the wild in order to diagnose or address critical issues — just checkout
master! Another benefit of ensuring that only in-production code is in master is that hotfixes – fixes for critical bugs in the wild – can very easily be made in isolation from ongoing development of the product.
develop branch contains the latest development code. In other words, it is the eventual destination of all features and bugfixes that will go into the next release. Since feature branches and bugfixes are independently developed, tested, and validated prior to merging them into
develop, the HEAD of this branch should, under ideal circumstances, always produce a “good” build with the latest validated features.
There are also a variety of temporary branch types in Gitflow which are created as necessary and destroyed when no longer needed.
Feature branches are where development of new features is performed. If your team is using a methodology like Scrum, then each feature branch generally represents a single user story. Multiple developers may work on a single feature branch at once, in which case their collaborative Git workflow on the feature branch will generally resemble the familiar single-branch workflow, with both developers pushing commits to the same branch.
Feature branches should follow a naming convention where an optional user story identifier and short hyphen-delimited summary of the branch are preceded by
feature/. For example,
feature/JIRA-50-new-login-ui. It’s worth noting that this naming convention does not follow Vincent Driessen’s original proposal, but it serves a few useful purposes:
- The prefix facilitates immediate identification of the purpose of the branch
- The front slash is interpreted as a “group” by many git clients, and also makes branches listed using the CLI align well with one another visually:
bugfix/JIRA-100-this-bug develop feature/JIRA-120-this-feature feature/JIRA-121-that-feature master
- A common prefix makes it extremely easy to setup a matching pattern in CI systems to automatically build temporary branches when commits are pushed to them, e.g.
As you may have guessed, bugfix branches are created for the purpose of fixing one or more related bugs. The use of bugfix branches should be restricted to only bugfix work as to avoid confusion. Although it’s possible for multiple bugfixes to be included on a single branch, it’s advantageous to keep these branches as small and concise as possible to minimize the chance of changes introducing unintended side effects.
In a similar naming pattern as feature branches, bugfix branches should begin with
bugfix/ followed by an issue number (if there is one) and short summary. For example,
If there is a critical issue in your product that’s affecting users in the wild, Gitflow makes it extremely easy to patch and release a new update. Remember how the HEAD of
master is always the current version in production? Well, in order to patch an issue without disrupting ongoing development, all you need to do is create a “hotfix” branch off of
master, fix the issue, test it, and merge back into
develop. The patched build is created from
master, and all future builds will also include the patch since it was merged back into
develop as well.
No surprise here: hotfix branches should be named starting with
hotfix/ and followed by an issue number and a short summary of the fix. For example,
A pull request is an abstract concept that isn’t actually part of Git, but rather a process tool that can be used to validate code changes made on branches before merging them into a stable branch. The idea of a pull request became well known in the collective consciousness of software developers largely due to its implementation on GitHub.
The purpose of a pull request is to indicate to the rest of the development team (or to the maintainers of the codebase) that you have a branch with new changes that you’d like to have merged into a stable branch. This gives other developers an opportunity to review the code changes and gives QA and/or a continuous integration system an opportunity to validate the quality and integrity of the build with the new changes integrated.
In most Git frontends such as GitHub, Bitbucket, or Stash, a pull request is represented by a webpage with three main components:
- A timeline of events in the pull request, which shows a chronological history of code review comments and commits that have been added or deleted since the pull request was opened. Sometimes the timeline also has links to issue tickets or other pull requests referenced by the pull request or vice versa.
- An overall line-by-line diff of the code in the branch with the code in its merge destination. This is often supplemented by a commenting system which allows comments to be made on a file as a whole or on a particular line of code.
- An up-to-date list of commits that will be merged into the destination if the pull request is accepted, each with individual line-by-line diffs.
Here is an example of a merged pull request from a popular open source repository on GitHub.
In Gitflow, when developers are finished working on a temporary branch, a pull request should be created for the branch and reviewed/validated by the team before being merged into its destination (typically
develop). This provides a standardized quality checkpoint for all code changes and helps ensure that permanent branches are always stable.
Sometimes a pull request will not be able to be merged due to merge conflicts. There are two primary ways to address this: merging from or rebasing onto the destination branch. Which you use is a matter of preference; merging can be easier but litters your Git history with bidirectional merge commits, while rebasing creates a cleaner git history but can be more tedious and requires force-pushing to the remote server once completed, since it involves rewriting commits. Force pushing can be scary and requires anyone else working on the same branch to carefully handle updating their local copy, so if you choose to rebase, communication is key!
As mentioned a few times previously, Gitflow is highly effective when it comes to release management. Let’s walk through an example to see how it works.
Suppose the development team at the hip software company Deuterium, Inc. consists of two sub-teams, Team A and Team B, working on the company’s flagship product, Stratosphere. Each team could be made up of multiple people, or each team could just be one developer.
- Version 1.0 of Stratosphere is already out in the hands of customers.
- Team A is in charge of preparing version 1.1 for release.
- Team B is in charge of working on a feature that isn’t finished yet, but is planned for version 1.2.
Here’s how Gitflow makes this type of scenario not only possible, but also easy to manage:
- Team A creates a new branch off of
develop, which at this point includes all of the new features and bugfixes that will go into version 1.1.
git checkout -b release/v1.1
git push origin release/v1.1
- At this point, the code for release version 1.1 is effectively frozen.
- Team A encounters a stop-ship bug in the code for version 1.1, so they create a new bugfix branch from
git checkout release/v1.1
git checkout -b bugfix/JIRA-150-stop-ship
git push origin bugfix/JIRA-150-stop-ship
- While Team A is working on the stop-ship bug, Team B manages to finish, test, review, and validate their new feature before version 1.1 is released yet. They merge their feature into
develop, push the result, and delete the feature branch.
git checkout develop && git pull
git merge --no-ff feature/JIRA-149-awesome-thing
git push origin develop
git push origin :feature/JIRA-149-awesome-thing
- Note: If using a Git frontend, merging the pull request from the web interface would accomplish all of the above automatically. Also, the
--no-ffis to ensure that the merge generates a merge commit, serving as a record in the commit history that the branch was merged.
developnow includes the new feature, but since
release/v1.1is isolated, the release build is not affected by this.
- Team A fixes the bug, so once they have validated and tested it, they merge the bugfix branch into the release branch and delete the bugfix branch from the server.
git checkout release/v1.1
git merge --no-ff bugfix/JIRA-150-stop-ship
git push origin release/v1.1
git push origin :bugfix/JIRA-150-stop-ship
- Note: once again, merging a pull request in a Git front end would do all of the above.
- Team A validates the release build with the bugfix and is ready to ship. They merge the release branch into both
develop, delete the release branch, and create a new tag for the release.
git checkout master && git pull
git merge --no-ff release/v1.1
git tag -m v1.1
git push --tags origin master
git checkout develop && git pull
git merge --no-ff release/v1.1
git push origin develop
git push origin :release/v1.1
- At this point,
mastercontains the in-production code for version 1.1, and
developnow includes both the bugfix implemented by Team A and the new feature for version 1.2 implemented by Team B
This type of (dare I say) agile release management would be orders of magnitude more difficult if not using a branching version control workflow, and effectively demonstrates the power of Gitflow for streamlining release management.
Git is an extremely powerful tool which can serve to either complicate or streamline your development team’s version control and release management processes. By making good use of Git’s easy and functional branching capabilities in an enforced and structured workflow, many of the common pain points of building software (at least from a process standpoint) can be addressed in an effective and simple way.
Here are a few other resources on Gitflow to check out if you want to dive deeper. Happy branching!