Code Craft

Software is equal parts Art, Craft, and Engineering

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 release/xxx branches.

Permanent Trunk Branches

There are up to four permanent “trunk” branches in increasing level of stability, quality and deployment:

  1. develop is for functionally complete code which is suitable for undergoing QA.
  2. qa is for the support team quality assurance process. It will be merged from develop and fixes landed only to address the needs of QA as requested by the support team.
  3. release is the early-adopter production branch. It will be merged from qa at the discretion of the support team.
  4. stable is the general production branch that the majority of customers will be on. It will be merged from release at the discretion of the support team.

There are projects where is sensible to use only some of these trunks. Those projects may omit the qa, stable and release branches as appropriate.

Normal Development

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.

Downstream Fixes

Summary:

  • 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 develop.
  • 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.

Detailed Steps:

  1. Check out the most downstream trunk that needs the fix and create a branch named dev@abc/ii-xxx, where abc is the first three letters of the target branch, ii is your initials and xxx is a concise description of the area affected (just like a normal branch). Think of it as, for example, “development at release”.
  2. 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.
  3. Make and test your change locally on the side-branch.
  4. 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.
  5. 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 origin.
    • 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 origin trunk would cause all your commits to be new, creating new commits with identical contents on the trunk (compared to downstream trunks).
  6. Delete the dev@abc side-branch.

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 dev@abc branch.

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:

  1. Checkout stable and rebase it from origin.
  2. Merge dev@sta/ii-xxx => stable.
  3. Merge stable => release.
  4. Merge release => qa.
  5. Merge qa => develop.
  6. Push stable to origin.
  7. Push release to origin.
  8. Push develop to origin.
  9. Delete dev@sta/ii-xxx and rebase or merge any of your own side-branches as you wish.

Other Details

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.

Versioning

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.