A git branch workflow
In this post I present a git branch workflow that has been working for years. By making it public, I hope it will help other coding monkeys get bananas easily. I also hope for suggestions on possible improvements to this workflow.
The branches
Think about what we do when develop a software: create the project, add features, fix bugs and release. Pretty much everything for a dev. Our branch workflow covers all these tasks.
Master branch
At the core of this branch workflow, there is only one mainline branch:
master
. This is the development branch containing latest code. Because
master
branch is the default branch in git, developers can start development
right after a clone, without the need to guess where the work should start.
Besides master
branch, there are other branches to support development:
-
Feature branches.
-
Bugfix branches.
-
Release branches.
Feature branches
- From: master
- To: master
- Name: feature-*
Feature branches add new features to the software using transactions. A transaction is a series of commits chained together implementing a new feature. It more or less resembles a database transaction, if that sounds more familiar to you. We’ll give an example shortly.
Bugfix branches
- From: master, release
- To: master, release
- Name: bugfix-*
Bugfix branches fix bugs in the software using transactions. The definition of a transaction here is similar, but it implements bugfixes instead of features.
Note that bugfix branches can be forked from either master branch or release branches. However, a bugfix branch forked from master branch must be merged into only master branch, and one forked from release branch only release branch.
Release branches
- From: master
- To: none
- Name: release-*
Release branches contain code ready for release. Release branches are forked from master branch. They only accept bugfixes, not features.
An example
Here we give an example about how to use this branch workflow.
Use feature branches
First, let’s create the project:
cd /tmp
mkdir repo
cd repo
git init
Then, do an initial commit:
touch a.txt
git add .
git commit -m "initial commit"
Git log:
bf6102 (HEAD -> master) initial commit
Next, we add a feature using a feature branch:
git checkout -b feature-01
touch b.txt
git add .
git commit -m "b.txt"
touch c.txt
git add .
git commit -m "c.txt"
Git log:
* 914f10 (HEAD -> feature-01) c.txt
* 0ffd00 b.txt
* bf6102 (master) initial commit
To merge this feature branch into master branch:
git checkout master
git merge --no-ff feature-01
Here we do have a requirement that the feature branch must be a fast-forward of the master branch. This is trivially satisfied when master branch is the initial commit, but not in general.
Git log:
* 401626 (HEAD -> master) Merge branch 'feature-01'
|\
| * 914f10 (feature-01) c.txt
| * 0ffd00 b.txt
|/
* bf6102 initial commit
Now we add more features using additional feature branches:
git checkout -b feature-02
touch d.txt
git add .
git commit -m "d.txt"
Here, suppose another developer is adding a different feature to current master branch, and checks in faster:
git checkout -b feature-03 master
touch e.txt
git add .
git commit -m "e.txt"
git checkout master
git merge --no-ff feature-03
Git log:
* 9d30da (HEAD -> master) Merge branch 'feature-03'
|\
| * 674545 (feature-03) e.txt
|/
| * 0a82dd (feature-02) d.txt
|/
* 401626 Merge branch 'feature-01'
|\
| * 914f10 (feature-01) c.txt
| * 0ffd00 b.txt
|/
* bf6102 initial commit
Now back on branch feature-02
. If we simply merge it into master
branch, the
resulting commit history would be ugly:
git checkout master
git merge --no-ff feature-02
Git log:
* 6757ad (HEAD -> master) Merge branch 'feature-02'
|\
| * 0a82dd (feature-02) d.txt
* | 9d30da Merge branch 'feature-03'
|\ \
| |/
|/|
| * 674545 (feature-03) e.txt
|/
* 401626 Merge branch 'feature-01'
|\
| * 914f10 (feature-01) c.txt
| * 0ffd00 b.txt
|/
* bf6102 initial commit
This is where the above-mentioned requirement comes: We require that the feature branch must be a fast-forward of the master branch. To satisfy this requirement, we need to rebase the feature branch onto the master branch before the merge:
git checkout feature-02
git rebase master
Git log:
* a6a710 (HEAD -> feature-02) d.txt
* 9d30da (master) Merge branch 'feature-03'
|\
| * 674545 (feature-03) e.txt
|/
* 401626 Merge branch 'feature-01'
|\
| * 914f10 (feature-01) c.txt
| * 0ffd00 b.txt
|/
* bf6102 initial commit
Now we can merge:
git checkout master
git merge --no-ff feature-02
Git log:
* 9b1f85 (HEAD -> master) Merge branch 'feature-02'
|\
| * a6a710 (feature-02) d.txt
|/
* 9d30da Merge branch 'feature-03'
|\
| * 674545 (feature-03) e.txt
|/
* 401626 Merge branch 'feature-01'
|\
| * 914f10 (feature-01) c.txt
| * 0ffd00 b.txt
|/
* bf6102 initial commit
This workflow has several advantages:
-
A clean and descriptive commit history.
Because feature branches are merged one by one, the history consists of at most 2 columns and displays nicely in git log graph.
-
An overview of merged features.
To show merged features in order, run:
git log --first-parent master
Git log:
* 9b1f85 (HEAD -> master) Merge branch 'feature-02' * 9d30da Merge branch 'feature-03' * 401626 Merge branch 'feature-01' * bf6102 initial commit
-
Easily revert a feature.
For example, if we want to revert
feature-03
, run:git revert -m 1 9d30da
Git log:
* 815d20 (HEAD -> master) Revert "Merge branch 'feature-03'" * 9b1f85 Merge branch 'feature-02' |\ | * a6a710 (feature-02) d.txt |/ * 9d30da Merge branch 'feature-03' |\ | * 674545 (feature-03) e.txt |/ * 401626 Merge branch 'feature-01' |\ | * 914f10 (feature-01) c.txt | * 0ffd00 b.txt |/ * bf6102 initial commit
Use bugfix branches
The use of bugfix branches is very similar to feature branches, except that they also branch off and on release branches (in addition to master branch).
Use release branches
We use release branches to release our software. For example, we can release
commit 401626
as version 1.0
by forking a release branch from this commit
and tagging it using semantic versioning:
git checkout -b release-1.0 401626
git tag -a 1.0.0
Now this release branch has its own life:
-
It only accepts bugfixes, not features.
-
It will never be merged back into
master
branch, or any other branch.
Git log:
* 815d20 (master) Revert "Merge branch 'feature-03'"
* 9b1f85 Merge branch 'feature-02'
|\
| * a6a710 (feature-02) d.txt
|/
* 9d30da Merge branch 'feature-03'
|\
| * 674545 (feature-03) e.txt
|/
* 401626 (HEAD -> release-1.0, tag: 1.0.0) Merge branch 'feature-01'
|\
| * 914f10 (feature-01) c.txt
| * 0ffd00 b.txt
|/
* bf6102 initial commit
If we find a bug in this release branch, we fix it and bump the
PATCH
version number:
git checkout -b bugfix-01 release-1.0
touch c.fix
git add .
git commit -m "c.fix"
git checkout release-1.0
git merge --no-ff bugfix-01
git tag -a 1.0.1
Git log:
* d0a12e (HEAD -> release-1.0, tag: 1.0.1) Merge branch 'bugfix-01' into release-1.0
|\
| * 06973c (bugfix-01) c.fix
|/
* 401626 (tag: 1.0.0) Merge branch 'feature-01'
|\
| * 914f10 (feature-01) c.txt
| * 0ffd00 b.txt
|/
* bf6102 initial commit
If this bugfix also applies to master branch, apply it there as well:
git checkout -b bugfix-02 master
git cherry-pick release-1.0^1..release-1.0^2
git checkout master
git merge --no-ff bugfix-02
Git log:
* fbfe4d (HEAD -> master) Merge branch 'bugfix-02'
|\
| * 1b4c2d (bugfix-02) c.fix
|/
* 815d20 Revert "Merge branch 'feature-03'"
* 9b1f85 Merge branch 'feature-02'
|\
| * a6a710 (feature-02) d.txt
|/
* 9d30da Merge branch 'feature-03'
|\
| * 674545 (feature-03) e.txt
|/
* 401626 (tag: 1.0.0) Merge branch 'feature-01'
|\
| * 914f10 (feature-01) c.txt
| * 0ffd00 b.txt
|/
* bf6102 initial commit
This commit history is still quite clean and descriptive.