Git V: An Optimal Git Branching Model

git v branching

Introduction

Git V is a branching model. In other words, it’s a way for teams of humans working in parallel on software to serialize their work into useful and organized streams of commits.

At a high level, Git V divides work into two primary streams:  destabilizing work (master) and stabilizing work (release branches). To keep the commit graph clean, Git V also takes advantage of specific merge mechanics (fast-forward) and pull-request policy (mandatory squash).

git-v-full
Example repo running Git V with two active release branches (including merge-backs).

Primary Mechanics of Git V

It’s called “Git V” because that’s how it looks. In a typical repository running “Git V” there are two main branches: master and the current release branch. They diverge from each other at a join point, creating a “V” in the commit-graph. The two branch types receive two distinct classes of work:  destabilizing work goes to master,  and stabilizing work goes to the release branches.

git-v-simple
The two mainline branch types when using the Git V are “cutting edge” and “reinforcement.” These map to master and release/*, respectively.

Here are the mandatory and optional requirements to implement Git V in your shop:

  • Mandatory: The cutting edge branch is master. Potentially destabilizing work goes here. New feature development, refactors, and particularly deep bug fixes should always go straight to master. Of course, developers should also strive to catch as many bugs as they can at this stage, and so many bug fixes might also exclusively hit master.
  • Mandatory: The reinforcement branches follow the pattern:  release/* 
    Stabilizing work goes to these branches. Stabilizing work should consist primarily of maintenance work and bug fixes. The working philosophy one should employ when pushing work to these branches is, “If it ain’t broke, don’t fix it.”  Also, “What’s the lowest-risk way I can make the fix?”
  • Mandatory:  Apply cascading merge-backs for *every* stabilizing commit to prevent regressions. Yes, you should do this for every single commit to a release branch! This is easiest to accomplish using something like Bitbucket Server’s “automatic merge” feature, but you can also do it manually. Cherry-picks are also an option, but I prefer merges since this helps highlight their special nature (cherry-picks are harder to distinguish from regular commits).
  • Recommended: For stabilizing work, target the oldest reasonable release branch that makes sense. For example, even though your bug was reported against version 2018.03.25 of your app, the bugfix makes sense as far back as 2017.08.22. In that case, try committing against your release/2017.08.X branch. Apply merge-backs to get the fix into your release/2018.03.X branch.
  • Recommended:  Keep the number of active release branches to a minimum. Try to migrate your customers off your oldest release branches as fast as you can!
  • Recommended: Enforce a squash policy to keep work cohesive and to keep your history clean and easy to understand. A squash policy also makes reverts and cherry-picks much easier to execute. But avoid squashing with git rebase –interactive since it completely ignores merges during its squash calculation. In-place-squash (via git reset –soft) or merge-squash are much better.
  • Recommended: Enforce a fast-forward policy and only allow merge-commits between the mainlines (master and release/*). This will further improve clarity and reduce noise in your history.

Note: I suspect that fast-forward policy breaks down once you have 200 or more developers per day actively pushing their work to a single shared master branch. Nonetheless, I highly recommend a fast-forward policy to keep history clean as long as such does not devolve into a perpetual rebase fight.

Off Stage Mechanics

These are not really part of the primary mechanics of Git V, but I recommend these for any PR (pull-request) based workflow.

  • Recommended:  Allow developers to push anything they like (including force-pushes) to branches outside the “master” and “release/*” patterns. These become the short-lived feature branches developers use to PR work into the mainlines. Personally, I like naming these after JIRA tickets (e.g., bugfix/TKT-101, feature/TKT-102, etc…).  Longer-lived experimental branches are also fine as long as developers remember to sync-merge master into them often (“git merge origin/master”). A final in-place squash (via git reset –soft) bakes in all those sync-merges. Force-pushes to all branches in here are fine as long as teammates always pull with “git pull –rebase.”
  • Recommended:  Run CI builds for all branches, and only let PRs merge if the build is successful. BUT only do this if you have build-status propagation in place! Otherwise, the squash and fast-forward policies above will cause build statuses to get constantly cleared. Also, implementing CI builds for all branches is hard on the CI server. Make sure you have the sufficient disk, ram, and CPU capacity before turning this on.
  • Recommended:  Require at least one code-review per-PR.  Personally, I’m a fan of BYORS/ARA. That is, “Bring your own review style / anyone-reviews-anyone.” Everyone is free to review however they like under this scheme, and juniors can review seniors and vice versa. The only rule under this scheme is you’re not allowed to review yourself!

While stricter review policies will result in even higher quality, such also require increased effort and tend to particularly distract senior engineers. I suspect a lightweight approach such as BYORS/ARA buys you the largest quality bang for the overall corporate effort expended.

BYORS/ARA is inappropriate for software projects where high quality is critical (e.g., flight control systems, medical equipment, etc).

How To Implement Git V

Here are the steps for deploying Git V to your environment (these steps assume you are the git guru for your team).

  1. Set things up, so master is your active development branch.
  2. If you’re running one or more production branches, rename them, so they match this pattern:  release/*

    Personally, I use a Year-Month pattern for my release branches, just like Ubuntu does. E.g., “release/2018.03.X” is a typical release branch in my repos. For monorepos, adjust the pattern to: release/[project]/
  3. Configure your git server to best support the Git V branching model.  

Instead of trying to train your staff to use Git V, it’s much easier to just configure your central Git server to only allow sanctioned Git V operations and block non-compliant actions. Your staff will quickly figure out how Git V works because your Git server won’t allow them to do otherwise!

At a high level, the key config you want in place should cover the following:

  • Pushes to master or release/* should require PRs (pull-requests).
  • PR merges should use “–ff” when possible.
    (Avoid “–ff-only” but also avoid “–no-ff”).
  • BUT if you are allowing “–ff” merges, then you also need to detect and block foxtrot merges.
  • Enforce a fast-forward policy for new commits to master or release/*.
  • Enforce a squash policy for PR’s merging into to master or release/*.
  • Enable some type of automatic merge-back cascade for the release branches if possible.
  • These rules above should NOT apply to merges between release/* and master. (e.g., once work has already landed in release/* or master, then it no longer needs to be squashed or rebased to transition to other mainline branches).

How To Implement Git V with Bitbucket Server

Since I’m very familiar with on-prem Bitbucket, I’ve included very specific instructions below for implementing Git V in that environment:

  1. Install Bitbucket Server or Bitbucket Data Center.
  2. Install the Control Freak for Bitbucket Server add-on.
  3. Enable the following global settings from that add-on:
Git v with Bitbucket server
(Note: these leverage each repo’s “Branching Model” configuration, in particular the definitions for master and release/*).

4. For every project in your Bitbucket instance, enable “Automatic Merging.” It’s far fewer mouse clicks if you do this at the project level instead of the repository level:

Branching Model –> Automatic merging –> [x] Enable Automatic Merging

automerge - branching model

Similarly, for every project, adjust the “Merge Strategy” to disable the “–no-ff” strategy and enable the “–ff” strategy. At the very least, make sure “–ff” is the default:

merge strategies

5. If your policy is to block PR’s on failed CI builds, we strongly recommend also installing Bit-Booster – Rebase Squash Amend to avoid rebase fights thanks to the “build status propagation” feature.

This add-on also helps make squashing easier since you can squash directly on the PR screen when this is installed.

Note: On teams of 200+ developers working on a single repo (typically a monorepo), the fast-forward policy can become a bottleneck. Disable the fast-forward policy if you notice developers rebasing repeatedly just to get a merge in.

Git V – FAQ

Question: I applied a bugfix to ‘master,’ and now I realize it really should have been applied to an older release branch.  How do I fix this?

Answer: Cherry-pick it to the release branch.

Question: I want to know the exact process a developer went through to develop a particular feature, but the history was squashed. How can I reconstruct the true history?

Answer: Some people prefer a more accurate history of exactly how a feature was developed in a branch. Git V is incompatible with those people because of how it aggressively sanitizes history to make history more succinct, focused, cleaner, and easier to understand.

Comparison: Git V vs. Git Flow

Git Flow was ground-breaking in its time, and it was a great initial map for teams trying Git for the first time. If you look closely, you’ll see Git Flow makes the same primary classifications: destabilizing work (Git Flow designates a ‘develop’ branch for this) and stabilizing work (“release branches” in Git Flow terminology).

My main complaints about Git Flow:

  • Messy commit graph.
  • One too many mainlines.
  • Production (master in a Git Flow environment) is not really the best default branch you want new staff starting from when they clone for the first time.

Git Flow also does not properly anticipate multiple active release branches, though such is common in practice.  If version 0.3 is already in production, where does 0.2.1 go (also in production) in the Git Flow map?

Comparison: Git V vs. Release Flow (Microsoft)

Git V is very similar to Microsoft’s Release Flow branching model. There are a few cosmetic differences and one significant difference that some might prefer!

The significant difference: Release Flow mandates that all stabilizing commits must land in master first before they are allowed to propagate to release branches. This is the inverse to Git V’s rule (stabilizing commits must land in a release branch first).

I suspect the Release Flow approach here allows bugfixes to receive more smoke testing in a highly active environment (presumably many devs build & run master every day). It also eliminates a common source of regressions (missed back-merges to master).

The downside to Release Flow’s master-first policy is it forces a cherry-pick, which makes an automatic merge-back cascade impossible.  A hybrid approach could require the cherry-pick target the oldest reasonable release branch, in which case an automatic merge-back cascade could bring the fix to the remaining release branches.

The cosmetic differences: Release Flow does not mandate a squash policy or a fast-forward policy.

Comparison: Git V vs. Linux Branching Model

The Linux Branching Model employes a hierarchical trust model. Lieutenants and sub-lieutenants responsible for particular subsystems review and (publicly) sign-off on commits from developers, and use merges to encode this trust structure. Even Linus Torvalds is said to not always trust himself, and sometimes passes off his own commits to a lieutenant so that it passes through the proper review channels before it finally makes its way back to him for his final merge into his mainline.

The Linux model aims for maximum patch throughput with maximum patch quality through intensive public code-review by senior engineers assigned to specific ownership areas.

In the Git V model, review happens external to the branching model through PR-based systems like Bitbucket or Github. Git V is able to simplify the resulting git history by completely avoiding this merge-based review support baked into Git by its original designer.

The benefit of Git V here is a simpler commit graph. Also, the review and trust relationships between your staff can be more dynamic and fluid compared to the Linux model. The downside is your review metadata is stored in a separate system and thus may be harder to access. (E.g., answering the question “who signed-off on this commit?” is trickier in a PR based system).

Conclusion

Git V divides work into two main branch classes:  destabilizing cutting-edge work, and stabilizing reinforcement work.  It leverages automatic merge-backs to avoid regressions. It empowers developers to run their features branches however the like, while promoting fast-forwards and squashes to keep the mainlines neat and tidy.

Additionally, the three main headaches that would normally arise from implementing Git V are eliminated thanks to a pair of Bitbucket Server plugins:

  1. Allowing “–ff” merges is critical if you want to run a fast-forward policy, but it also opens the door for foxtrot merges. Fortunately, these can be blocked by enabling Control Freak’s foxtrot prevention.
  2. Most merge controls force all merges into a target branch to subscribe to the defined policy.  And so it’s difficult to implement policy to enforce fast-forwards into the mainlines, while also allowing regular merges between the mainlines.  Fortunately, the Control Freak plugin does allow this exact setup.
  3. Mandating a fast-forward merge policy keeps the commit graph neat and tidy. But there’s an unintended consequence:  PRs usually need to be rebased before they can be merged under such a scheme. While rebases themselves are typically quick, the rebase also clears the build status. If your PRs are CI-integrated, and require successful builds, a fast-forward policy can result in severe productivity slowdowns (since each rebase will trigger a new build). Fortunately, the “Build Status Propagation” provided by Bit-Booster – Rebase Squash Amend mitigates this completely.

Final Piece of Unsolicited Advice

If you do find yourself in a Git V shop or any shop that permits force-pushes, please remember:

Always use git pull –rebase, because it always does the right thing!

Regardless of whether you end up using Git V or not, I hope you found this to be a nuanced and interesting blog post on Git!  Thanks for making it this far!

Julius Musseau

About the Author

Julius Musseau

Julius Musseau, co-founder & CTO. Senior architect and developer with strong academic background and roots in the open source community. Contributor to a number of important open source projects.