Git is the most relevant source code management system as of today. In this blog post, we explain our set of useful conventions that we follow when working with branches, tags and commit messages. We consider it suitable for small teams of up to five developers.

Our git repositories start with an empty initial commit. Here is quick boilerplate that we always use:

1
2
3
4
5
6
mkdir project
cd project
git init
git checkout --orphan master
git commit --allow-empty -m "Initial commit"
git checkout --orphan develop master

Now, we make commits to the develop branch while we work on the project. When we reach a stable state which should be deployed to production, we merge it into the master branch. To keep the master branch uncluttered, we use a non-fast-forward merge here. When we say merge, we always mean non-fast-forward, because a fast-forward merge is equal to a branch reset (while keeping uncommited changes, of course).

Feature branches

Feature branches group commits that belong to the same feature. They spin off from develop, and we try to keep feature branches as small as possible and to merge them back into develop quickly. They help to structure collaboration when multiple developers are working on the same code base, too.

The name starts with ‘feature-‘, followed by an issue id from your issue tracker, if you have one, followed by a short title.

Sometimes, a feature branch get stuck, because the developer is waiting for external feedback or information. If we are working in a small team, we occasionally stack feature branches in that case. That way, we can avoid merge conflicts later on, plus a developer can rely upon features that have not been merged into develop yet.

Another frequent case is that anywhere within a feature branch, the developer adds a bug fix, a whitespace fix or another general improvement which is not related to the feature at all. We try to get those commits into the develop branch by moving them ‘down’:

If this is not possible, we move those commits on top of our feature branch. Right before merging the feature branch, we move them on top, exclude them from merging and finally rebase them onto the recent merge commit.

To keep track of those commits which require special treatment later on right before merging, we add a prefix (e.g. ~develop) to the commit title. Of cause, this prefix can be removed after rebasing.

Hotfix branches

Unlike feature branches, hotfix branches spin off from master instead of develop. They have a similar naming pattern, but start with ‘hotfix-‘. They merge back into master to quickly release a new fixed stable version, but into develop, too.

Support branches

We use support branches to maintain old releases where needed. They branch off from master and we give them their very own hotfix branches.

If we need that particular fix in develop or another support branch as well, we either cherry-pick it, or we let the hotfix branch off earlier from master and merge as required.

Force push

We don’t think that force-pushing is always evil. Instead, it helps us to keep the history clean when something has gone wrong. A few things should be kept in mind:

  • Always use --force-with-lease! This way, you can completely eliminate the lost-update scenario.
  • Be careful when force-pushing larger trees. Your collaborators must be able to rebase their local changes.
  • Do not force-push the master branch or tags: This would rewrite your release history, but releases should be ‘final’, the next section explains why.

Tags

In our software projects, a tag denotes a released version, so they’re allowed only on master and support branches. Tags may trigger special CI behavior, e.g. a deploy to a production stage or a gem upload to rubygems.org. The tag description can contain a handwritten changelog, which can be used later on in the CI process. We’ve tried to generate changelogs automatically from git commit messages, but that way, they tend to be verbose and unhelpful.

All tags have a three-numbers format, separated by dots. The first number is the major version, which we increase when an interface has changed in an incompatible manner, e.g. a configuration variable has been renamed or has changed it’s default value. The second number is for compatible changes, e.g. a new feature or a design improvement. The last number is for bugfix releases that incorporates fixes for something that should have been working already.

If you release quickly and often and tend to change interfaces, you may get a lot of major versions with that versioning model. That’s ok!

Read on

Regarding commit messages, we have nothing to add to Chris Beam’s excellent blog post.