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.

References