This document is meant to serve as a repository of best practices for large, multi-environment source control management.
Table of Contents
- Introduction
- Rules of the Trunk
- The Basic Process
- Creating a topic branch
- Releasing 1. Preparation 2. Rebasing 3. Resolving conflicts 4. Cleanup 5. Updating topic branches 6. Tagging
- Managing Environments
- Introducing topic branches
- Reset, rebase, re-merge
- Tips and Tricks
- Git setup
- Automated reset
- Rewriting history
The first thing to understand when trying to make a clean git project is that there can only be one trunk. Most commonly this branch is referred to as master
, the default branch name in git. This branch serves as the source of truth in your project. Everything should be based on your trunk, and defer to your trunk when messes are made. When this idea is employed correctly, it gives us a lot of power to manage our codebase.
To properly use a trunk branch, a couple of rules need to be followed.
The first rule is clear: all branches must use the trunk as a base. This is fairly obvious when making topic branches - everyone should already be branching off master
to make features, bug-fixes, etc. The emphasis however, is on all branches, including environment specific branches, such as develop
. This takes a lot of care and planning from the developer, but can be done, and will be explained in depth later on.
The second rule is a repeat of the first, in essence. If the first rule is always true, a merge of the trunk into the branch would be a no-op. But the trunk can be updated and topic branches can fall behind. The point of this rule is to enforce the use of rebase
over merge
when dealing with the trunk. If you instead merge trunk into your topic branch, the first rule will be broken, as your topic branch's history is merged with your trunk's history, rather than being based upon it.
The last rule enforces a linear history in your trunk branch. "Commits from a single parent" means no merges commits. Merge commits have two parents, one from both branch. This rule is designed to keep the history of the trunk branch clean, as it serves as the source of all branches.
The basic git process for most projects uses master
for production releases, other branches such as develop
for environment releases, and topic branches named for the JIRA issue ticket. For the basics, we will pretend the other environment branches do not exist, only the trunk and topic branches.
According to the first rule, all branches should be based off the trunk. So, after you ensure that you have the latest version of master
in your local repository, you should use the following command to create all topic branches:
git checkout -b topic master
After you add a few commits to your topic branch, it should look something like the following:
E---F---G topic
/
A---B---C---D master
Clearly, the topic
is cleanly based on master
, no problems so far. So let's create another topic branch, for the next feature:
git checkout -b topic2 master
And after a few commits, we will have something like this:
H---I---J topic2
/
| E---F---G topic
|/
A---B---C---D master
Both branches are still based off master
, as running a quick git rebase master
will confirm. Now we are done making changes, and want to release topic
and topic2
.
Currently all releases are done through individual merges to master
. But you might have noticed that this breaks rule #3 of dealing with trunks. The trunk branch should not contain merge commits, only fast-forward merges are acceptable. In order to achieve this, we create a release branch.
First we create a release branch off our trunk:
git checkout -b release master
Then before we start the release process, we ensure all the branches we want to release (topic
and topic2
) are up to date with our trunk. Following the second rule, we do this with rebase
:
git checkout topic
git rebase master
git checkout topic2
git rebase master
Now we need to get the commits from our topic branches, into the release
branch. The release
branch is a special branch, however, since it will eventually become a part of the trunk. This means we can also not merge to this branch either, as those commits will carry over into our trunk. So we instead use rebase
. Using rebase
, you can resolve commits from many sources into a linear history on a branch, without changing the topic branch.
git checkout release
git rebase topic2
git rebase topic
Like doing a merge
, a rebase
can also have conflicts if two files are edited in different branches. This can cause some small inconvenience when creating a release branch. Let's pretend that the branches topic
and topic2
both edit a shared configuration file. When doing the release rebasing process, a conflict appears:
error: could not apply sha12345...
When you have resolved this problem, run "git rebase --continue".
If you prefer to skip this patch, run "git rebase --skip" instead.
To check out the original branch and stop rebasing, run "git rebase --abort".
Could not apply sha12345678901234567890... Changes configuration file in topic
From here is gives you two options to resolve: using rebase --skip
or rebase --continue
. Using rebase --skip
is straightforward - it will skip this commit during the rebase. This would be useful if topic
and topic2
both had made the same change to the configuration file, thusly only adding one commit for it in the history.
Otherwise, this is the same situation as a regular merge
conflict. Git will tell you the files in conflict, and uses the same notation in the files to show you where. Once you resolve all the conflicts, the following commands will continue the rebase along.
git add .
git rebase --continue
Once all topic branches have been rebased into the release
branch, you should have the following structure.
H---I---J topic2
/
| E---F---G topic
|/
| E---F---G---H---I---J release
|/
A---B---C---D master
From here you can now do a fast-forward merge from release
to master
(also pruning the branches):
git checkout master
git merge release
git branch -D topic
git branch -D topic2
git branch -D release
to keep a linear history in your trunk branch:
A---B---C---D---E---F---G---H---I---J master
Let's pretend now that we had a thrid topic branch, topic3
, that did not make it into this release. Giving us this structure:
K---L---M topic3
/
A---B---C---D---E---F---G---H---I---J master
After each release you want to ensure all branches follow the first rule. In order to do this, rebase
the topic3
branch onto the updated trunk.
git checkout topic3
git rebase master
Any conflicts that need to be resolved become a part of the topic branch's history on a per commit level, as if it was always branched directly off the updated trunk. This keeps all changes sandboxed to their current branch. Now the structure should be:
K---L---M topic3
/
A---B---C---D---E---F---G---H---I---J master
It should become a part of everyone's release strategy to use tags as snapshots of the trunk. This way it becomes a simple task to view the trunk at different parts of the project's lifespan. To tag the trunk, use the following commands:
git checkout master
git tag v1.0.0
git push --tags
In theory, the above example is the complete process for development. We have a trunk with topics that, through a release process, eventually become a part of the trunk codebase. In reality, we have environments to maintain with different versions of the code to test against. The important thing to remember is, while these branches must follow the rules, they are not a trunk branch themselves.
Since environment branches are not trunks, they are allowed merge commits. After each topic branch is ready to be introduced to an environment, you are free to merge the brach in normally. Resolving conflicts on the environment branch is also ok. These conflicts will be resolved again when you rebase onto the release branch later, but now you can be aware of potential problems.
The most difficult part of managing environments is updating them after a release. Assume the following structure, directly after a release:
K---L---M topic3
/
| E---F---G---H---I---J---TX2---K---L---M---TX3 develop
|/
A---B---C---D---E---F---G---H---I---J master
where the TX
commits indicate merges of topic branches into develop
. These merge commits show that develop now has different history than the current trunk. Since environment branches are subject to the first rule, you need to reset each environment branch to the trunk. Doing a rebase
is not possible, because of the messy merge commits. The following will reset develop
to the current trunk:
git checkout develop
git reset --hard master
giving us this structure:
K---L---M topic3
/
A---B---C---D---E---F---G---H---I---J master, develop
Now we have two problems. First, topic3
is violating the first rule. This can be fixed by performing a rebase on all remaining topic branches after every release. Now we have this structure:
K---L---M topic3
/
A---B---C---D---E---F---G---H---I---J master, develop
Second problem, we are now missing code that used to be in develop: the commits from topic3
were overwritten in the reset
. To remedy this, we simply re-merge the topic branch back into develop. Giving us this final structure:
K---L---M topic3
/
| K---L---M develop
|/
A---B---C---D---E---F---G---H---I---J master
This needs to be repeated for all environment branches after each release. This ensures each branch follows trunk, instead of having its own unique history, to prevent environment specific issues.
Git can be made a lot easier with a proper environment setup, and some knowledge of how to perform some simple git magic.
Here is a .gitconfig file (located in ~/.gitconfig
) with some very useful aliases and configurations.
[alias]
co = checkout
s = status
clear = checkout -- .
last-diff = diff HEAD~1
hist = log --pretty=format:'%C(auto)%h%C(reset) | %s%C(auto)%d%C(reset)' --graph
dump = cat-file -p
reset-head = reset --hard HEAD
reset-trunk = reset --hard master
root = ! git log | tail -n 5 | grep ^commit | cut -d ' ' -f 2
tip = ! git log | head -n 1 | cut -d ' ' -f 2
b = "!f() { git checkout -b $1 master; }; f"
[push]
default = simple
Here is a script to automatically reset all environment branches for multienvironment projects. This can be run after each release, then only in-progress branches will need to be updated, and re-merged.
#!/bin/sh
git fetch
# reset branches
git co develop
git reset --hard origin/master
git push -f
git co qa
git reset --hard origin/master
git push -f
git co staging
git reset --hard origin/master
git push -f
git co sandbox
git reset --hard origin/master
git push -f
git co master
This blog post by Thoughtbot gives many good examples on how to fix small mistakes in your git history.