A Pragmatic Git Development Workflow
Set forth here is a Git workflow that has proven effective for all projects. The process is very similar to GitFlow, except that we use a set of permanent branches instead of multiple
Permanent Trunk Branches
There are up to four permanent “trunk” branches in increasing level of stability, quality and deployment:
developis for functionally complete code which is suitable for undergoing QA.
qais for the support team quality assurance process. It will be merged from
developand fixes landed only to address the needs of QA as requested by the support team.
releaseis the early-adopter production branch. It will be merged from
qaat the discretion of the support team.
stableis the general production branch that the majority of customers will be on. It will be merged from
releaseat the discretion of the support team.
There are projects where is sensible to use only some of these trunks. Those projects may omit the
release branches as appropriate.
All non-trivial development is done by branching
develop to private branch
dev/ii-xxx and merging back to
develop in as short a time as possible. The
ii is your initials and denotes ownership, the
xxx is a terse label of the general part of the system you are working on.
Private branches should generally be deleted upon merging, with few exceptions such as a long-running feature with multiple smaller useful functional deliveries.
Private branches for a series of small unrelated fixes, as when working on reducing a large pool of outstanding issues can be held open for a day’s work, but should be closed out and reopened or rebased at the first merge to
develop where the branch spans more than a day (this avoids a multitude of long-lived branches with a series of unrelated changes and is a concession to not needing a separate branch for every tiny change).
All private branches should either be merged or pushed to the server at least daily for backup and visibility to other devs. Do not forget to delete the server branch when you delete a local branch.
Developers must ensure that incomplete code is not merged to
develop. Incomplete code is that which would appear incomplete to the user. Useful stages of a large feature should be merged at any commit which provides what would be complete (though limited) functionality to the user.
Minor changes may be occasionally done directly on
develop without branching but must be complete before being committed to the central repo. A good example is updates to project information, supporting scripts, etc.
- Start at the bottom and merge up through the trunks.
- It is critical that any change ALWAYS makes it all the way back up to
- It is conceivable that some rare changes will become irrelevant at some point on the way up, but they still must be merged and conflicts resolved (perhaps discarding all changes) so that the upstream branches see all downstream commits.
- It’s important to do as much as possible locally before pushing anything to the central repo.
- Check out the most downstream trunk that needs the fix and create a branch named
abcis the first three letters of the target branch,
iiis your initials and
xxxis a concise description of the area affected (just like a normal branch). Think of it as, for example, “development at release”.
- You may wish to delete local copies of any trunk at this point. This saves you needing to remember to pull the trunk right before you merge into it, since you will need to freshly check it out from the remote first.
- Make and test your change locally on the side-branch.
- Merge your side-branch into the base trunk from which it was branched and then merge each trunk into it’s upstream predecessor, all the way to the top.
- Checkout the trunk, then rebase on origin, then merge the downstream trunk.
- Push the results to the repo from the base trunk all the way up (that is, bottom up).
- If the failing trunk is the base, you can simple rebase it onto
- If any other trunk push fails, pull the trunk again using a FF merge NOT REBASE, resolve any conflicts, merge that all the way up locally and then resume pushing from where you left off.
- Using rebase to pull the
origintrunk would cause all your commits to be new, creating new commits with identical contents on the trunk (compared to downstream trunks).
- If the failing trunk is the base, you can simple rebase it onto
- Delete the
There’s a very important principle that needs to be emphasized: No hot-fix can move downstream unless it is done on a
dev@abc branch, merged to the intended target and then all the way up to develop as normal. This must always be followed for EVERY fix.
This means that if you make a fix to some trunk and you subsequently discover it’s needed further downstream you must follow the procedure above to remake it to a lower trunk. For example, having landed a change on
develop that is also needed on
release, make a
dev@rel branch and follow the hot-fix procedure. You may be able to cherry pick the commit to the
dev@rel branch if it’s sufficiently similar (you can cherry pick and check the result, and rollback if it’s not worth it) but it’s still a new commit and as such it needs to be merged upstream to develop.
The only use for cherry-pick in this process is when the source commit is upstream and the target branch is a
The only time a
dev@abc branch can be deleted is when it’s been merged from its target all the way back up to
develop and the results pushed.
Pushing results example:
stableand rebase it from
dev@sta/ii-xxxand rebase or merge any of your own side-branches as you wish.
Fast-forward merges of features and fixes into the trunk branches are prohibited (this is to preserve clear and correct history, and the ability to apply them to the other permanent branches in a single commit). All team members are to take whatever steps their tool allows to ensure that merges to trunks are always done with a commit-merge.
Conversely, rebasing when pulling from central repository branches (origin) into your local copy is required, except from when pulling during an upstream merge where is prohibited. If you need appropriately named aliases to get this right, then, please by all means create them. If you forget but have not yet pushed the trunk, then revert and start over for that trunk. This prevents identical changes from appearing with different commit numbers and assists when ascertaining which trunks have which changes and in listing changes between trunks.
Whether you choose to use merge or rebase for pulling changes from a
develop to a dev branch depends on the situation, but generally rebase is the better option, in the opinion of the author. When code was written is not nearly as important and when it was admitted into a trunk, and generally speaking the history is more useful when related commits are grouped together. The author usually rebases his private branch, squashes any incidental commits and then merges into the trunk.
When a product does have a versioning system or build step, that is always to be done on a trunk prior to merging it to other trunks. You may well determine that versioned products have become obsolete. But projects for libraries, components and middleware generally will retain their versioning schemes.
This methodology commends itself well, in practice, to patching specific versions following it’s general principles. It works best if (a) you use a two level
major.minor scheme and (b) any given trunk is carrying only a specific major version so that the minor version can updated independantly. This implies that when version 2 is pushed from develop to release, version 1 is first pushed from release to stable, and afterward develop is immediately updated to 3.00. Likewise, later, 2 is pushed to stable and 3 is pushed to release and develop becomes 4.00. And so on.
Tags are very useful to permanently mark specific versions; use tree structured tags such as
V1/01 so that GUI tools can collapse the trees for irrelevant versions such that they do not have to deal with a list of hundreds of versions years later.