If you've ever worked on a team with more than a couple of people, you've probably been involved in a discussion about branching strategies. Git-flow, GitHub-flow, Microsoft-flow, and many others all try to minimize disruption and conflict when making changes to a large codebase.
Recently, I've been playing with a new strategy that may help your team avoid time-expensive merge conflicts and maximize commit history readability for easier diagnostics later on. I call it graph gardening.
- 🍃 New Work, New Branch
- 🌅 Rebase Early & Often
- 🌳 Back Up Branches
- 🔮 Predict the Future
🍃 New Work, New Branch
Any change, no matter how small, warrants a new branch in my version control system. That means every bug fix, every feature, and every tooling update. When I'm working on a feature and realize that a tooling update would make my life easier, it gets a new branch. That work doesn't live in my feature branch.
If you're playing along at home, a good self-assessment is to run
git branch -l. If your list looks like a kebab-cased log of the thoughts that you've had recently, you're on the right track. If your branch list looks like your backlog, you're getting there.
But I bet that you've probably made changes on some of those branches that aren't uniquely tied to a particular story. Do you remember which ones? If you do, what are you expelling from short-term memory to maintain that list in your head?
Using your brain makes sense for some things, but when you have a super-fast, super-redundant, fail-safe change graph system at your fingertips, using precious brain space to remember what changes are where seems wasteful.
🌅 Rebase Early & Often
The base commit of your current branch is probably not as important as you think. Far from being an intentional choice like the rest of your commits, the base commit was probably chosen by happenstance and timing. What was on
master when you started working on your feature? What was in progress, and what was finished?
The changes that you've made definitely depend on some of the lines in your base commit, but surely not all of them, and probably not all that many. So when I'm working on a feature branch and the parent branch moves on, I rebase my branch as soon as I notice.
Most of the time, it's a transparent operation. I run
git rebase master and move on. Occasionally, if I change code that was also changed on
master, I take the opportunity to resolve any conflicts. That way, when I eventually merge my changes back, it’s an easy merge.
🌳 Back Up Branches
Any time I’m about to make a change that can possibly result in data loss, I add a new branch or a tag to tell
git that I want to preserve my current state. Though it’s tempting to think of data loss as something that can only happen when running
rebase or other reflog-altering commands, I like to make backup branches any time I might lose context.
For instance, if I’m about to try my hand at a complicated merge, I’ll add a backup branch that points to the commit prior to the merge. That way, if the merge goes badly, I have an easy reference commit from which to recover.
If I’m doing a rebase that’s anything more than a basic squash of a fix-up commit, I check out a backup branch first. If the rebase goes well, I delete the backup branch and move on with my day. But if the rebase goes badly, I once again have an easy reference from which to recover.
In most cases, you can recover from a botched rebase without a backup branch, but doing so requires digging into the reflog, a dank cave whose dripping ceilings and hairpin turns flummox even the most seasoned of change graph spelunkers.
🔮 Predict the Future
Many branching strategies are designed to protect you and your team from upstream changes by specifying elaborate branch lineage rules. Hotfixes always descend from
master, features always descend from
Instead of fretting about lineage, I prefer to base my branches on the latest commit that will probably be merged into master. If we have a long-running feature branch (
new-sync-engine for instance), when I start a new feature, I’ll base my changes on
new-sync-engine. But I add a slight twist: Instead of basing my code on the latest commit in
new-sync-engine, I squash that branch into a single commit and base my changes off of that.
If I guess right, and
new-sync-engine gets merged into
master, I’m golden. I rebase my changes on
master, drop my single-commit copy of the
new-sync-engine branch, and move on with my life. If
new-sync-engine accrues more changes before I’m ready to merge, I cherry-pick the new commits into my branch, then do an interactive rebase with
git rebase -i and squash those commits into my placeholder commit from earlier.
This way, I’m able to use new concepts introduced in feature and bugfix branches without all the headache of managing a long string of prerequisite commits. Instead, I have a single point of integration. Rather than wading through the differences between my branch and the base branch at each commit in a long string, I just have to deal with them once.
Why Graph Gardening?
This strategy isn't for everyone. It assumes that you work on a small team with members whom you can trust to practice good shared branch hygiene. If you work on a large team with lots of in-flight branches, guessing which branches will be merged might not be a viable strategy. But if, like me, you work on a small team of awesome developers with good heads on their shoulders, it just might help make your development life a little easier.
Happy gardening! 🌱