Every other week, the Principle Studios Engineering team gets together and talks about relevant topics in the industry. Sometimes we have a group discussion on current events, sometimes we have multiple presenters showing small development technique improvements… and sometimes we have one larger presentation that takes most of the time.
This week, we discussed the Scalable Git Branching Model, specifically the Single Latest Version approach, which we use with many of our clients, and even our own website.
Matt DeKrey Presents: Git, Branches, and Remotes
Matt DeKrey kicked things off with a dive into the implementation of how
braching works in git. Because git is a distributed version control
system, it is built to keep track of the state of branches on other
computers, referred to as remotes. It does this with how it handles branching:
branches are pointers to commits. In any git repository, if you look in the
.git/refs/heads
folder, you’ll see every local branch is a file. If you look
at the contents of these files, the contents of the file is the commit hash.
When fetching from a remote, git retrieves the list of branches to track
locally, as well as a pack the objects from the remote that do not yet exist
locally.
Daniel Kurfirst Presents: A Scalable Git Branching Model
Daniel’s team follows the Scalable Git Branching Model. For his team, the most important part of the model is “Isolation until finalized”. Their client wants to control exactly which features are deployed in a given release, and releases can change even after the development and QA work for the features has been completed. This makes sense in context: we want to give our clients the agility to respond to changing business needs, however, it tends to cause issues with many other branching models that assume a PR means the feature is committed for release. We also don’t want to introduce dozens of feature flags for each project within the codebase.
To this end, Daniel’s team uses the following branch types:
- Service line: They only have one, which is called
main
. Nothing goes into it except when it reaches production, and every branch should have the latest commit frommain
in it. - Release candidate: They use Release Candidates to trigger automated builds
to deploy to dev, BAT, or UAT. When the client deploys an RC, it is merged
into
main
. - Feature: Each feature gets its own branch; these are merged together to form the release candidates. They receive the latest main when it is updated, but are otherwise kept separate from each other.
- Bugfix: Bugfix branches are made off of a feature branch and merged back to the feature to address bugs found in QA. If a bugfix branch is not merged before a feature is updated, the bugfix should be updated with the latest from the feature.
- Integration: Integration branches are used to resolve conflicts between individual features before merging into the release candidate; this helps them resolve conflicts once.
- Infrastructure: Code such as refactoring or new framework features that can be used by multiple features goes into an infrastructure branch.
Daniel then went through a list of common issues that engineers run into when working with the Scalable Git Branching Model.
Problem: Feature branch branched from a release candidate
While intuitive in many other systems, doing this means we have no clean way to isolate the new feature if client wants to cherry-pick certain features from rc branch for a production build request.
Solution: always branch from main, or a parent epic branch and add
upstream dependencies using git tools git add-upstream
.
Problem: Infrastructure changes made in a feature branch, or multiple infra changes done in the same infra branch.
When in the flow, can feel very intuitive to do this in a feature branch. However, this makes it difficult for developers to pull in infra changes in isolation in the future.
Solution: Identify infra changes quickly, create a new infra branch and make changes there. All developers should pull these into active feature branches as upstream.
Problem: Creating integration branches directly with a release candidate
Doing this can make sense and be a much quicker way to resolve conflicts, but violates the “isolation until finalized” principle. This is one of less-severe violations as the feature can still be cleanly isolated for a build request if necessary, however an integration branch that includes the rc cannot be re-used, and any conflicts will need to be re-resolved.
Solution: Identify the branch(es) where the conflicts originate, and integrate with those. Now you have an integration branch that can be re-used should, e.g. the same conflicts arise in another PR that includes the original features as upstream. The following commands gets the commit where the conflict originated and then turns that into the branch where you may resolve it:
git log origin/rc -- path/to/file.ts
git name-rev <result-of-last-command>
Note that this command isn’t perfect; this may give a different integration branch, but with some iteration you can get the right branch.
Problem: Bugfixes directly on an integration branch
This can make sense and be a much quicker way to resolve bugs from QA, but, again, makes it difficult to isolate a feature for a build request. This can also result in bugs re-occurring if those fulfilling the build request are unaware, which can damage perception of the team.
Solution: Fix the bugs on the feature branch instead. If integration branch was done properly (i.e. not based from a release candidate), feature with bug fixes can be pushed directly to the integration branch and a new PR opened.
Jeff Hand Presents: Feature Branch overview
The client served by Jeff’s team breaks stories down much smaller. As a result, they frequently have co-dependent features within a single release. Attempts to isolate by story led to a very complex web of dependencies that obscures the boundaries between branches, leading to developer confusion and ultimately breaking the isolation. Instead of each story being kept separate, Jeff’s team is moving to a “deployable feature” model, where stories that need to be shipped together use a single deployable feature branch. This significantly reduces the number of branches with upstream dependencies. It also means that infrastructure branches occur less often - since refactoring tends to be isolated to a single feature’s business logic area, it rarely affects multiple stories.
Thanks to our presenters!
Thanks so much to Daniel Kurfirst and Jeff Hand for presenting! We take the software development process seriously, seeking to follow our pricinples in the context of our client’s needs. Frequently, this means doing something slightly different than the industry standards. These refinements on our branching strategy allow us to meet the ever-changing demands of our client’s industries, and having new perspectives and adjustments to the workflow benefits everyone when we can share it.
We always appreciate seeing the work that our team does and geeking out over the details of software engineering; not everything can make it to the blog every week, but we hope you enjoy reading what can!