We ❤ Git and GitHub at Sparkbox. It’s our go-to version control system. We encourage clients who need a version control system to use Git and GitHub if possible. Just about everyone at Sparkbox touches GitHub at some point during a normal work day, whether that be through issues or commits. Our typical Git and Github workflow is to keep the
master branch in a “deployable state,” which means nothing goes into
master without a peer review and
master is usually auto deployed to a staging or QA server. Work is added to
master by creating feature branches and then pull requesting those branches back into
master. This approach works really well when the work for each feature branch can be accomplished in a week or two and only one or two developers are working in that branch at a time. Merging to
master in a regular cadence like this does not work well when a feature will take months and or involve many developers. In that case, one solution is to use a long-lived feature branch strategy. For the last couple of months, I’ve been working on a project that uses this strategy and I’d like to share how I prefer to work in it.
What is a Long-lived Feature Branch?
A long-lived feature branch (LLFB) can be useful when working on a feature that: shouldn’t be in the
master branch until it’s ready, will take a significant amount of time (e.g. a month or more) to complete, and involve several developers. This approach allows
master to avoid all the churn that happens in the feature branch. Remember, we typically consider
master to be “deployable” so that means
master can’t afford to have half-baked features in it. One necessity when working with a LLFB is keeping it up-to-date with
master. While we don’t want to muck up
master, we do want all the great changes and fixes that are coming in from other teams into
master in our feature branch. This also makes the final merge of the LLFB much easier.
Get the Latest
How do we update the LLFB with fixes and other new features from
master? The typical approach is to
git merge master in the LLFB. However, that creates a pretty gnarly looking log with many “merge commits” cluttering the history. The alternative is to
git rebase master in the LLFB. Rebase works by going back to the point where the LLFB split from
master and updating that point with
master and then “replaying” the LLFB commits on top of that. The LLFB branch work is now always happening on top of the latest work from
master. Now, the log shows a linear progression of work without any “merge commits.”
We Have A (New) Problem
The LLFB is now synced with
master. However, individual developers typically create personal feature branches off the LLFB and now need to create pull requests for their work. When a developer pushes up their personal feature branch, GitHub will interpret their old “pre-rebase” LFFB commits as new commits and report that there are conflicts between the personal feature branch and the LLFB. What happened?
git rebase happened. When
git rebase runs it “rewrites” the history, creating identical but different commits. Therefore, the LLFB technically no longer has the commits it had when the developer created the personal branch.
Rebase Uphill Both Ways
One option would be to
git rebase the personal feature branch off the LLFB and slog through all the conflicts and commits just like the LLFB did with
master. This hurts more and more as the LLFB lives longer. Remember as the LLFB gets periodically rebased off
master its commits get rewritten (Git uses different commit SHAs). This means when the personal feature branch is rebased off LLFB Git resolves—and finds conflicts in—all the commits since the start of the LLFB.
Cherry Pick to the Rescue!
Instead of fighting through all those commits, we can use
git cherry pick. Cherry-pick works by copying a commit or a series of commits from another branch onto the current branch. Instead of working bottom-up (or past to the present) like
cherry-pick works top-down (or present back to the past). Remember,
git rebase resets the personal feature branch to the branch point off LLFB and then replays the personal feature branch commits one at a time. With
git reset we can set our personal feature branch to current state of the LLFB and then drop the personal feature branch commits on top via
cherry-pick. So, the only conflicts will be in the personal feature branch commits. Sounds great, right? We can’t do this blindly. If we reset our personal feature branch to the LLFB we would lose the new commits. We need to take measures to avoid losing the personal feature branch commits and have them available to
Backups Are Good
Before jumping into any potentially hairy Git situation, it’s smart to create a copy of the personal feature branch first, whether that be a literal duplicate branch or pushing the branch up to origin. The goal is to have the new commits in two places. Once backed up, it’s time to start on the personal feature branch. It’s important to get back to the latest LLFB state (be sure to do this when the personal feature branch is checked out),
git reset LLFB --hard. This will coerce the personal feature branch (e.g. discard commits) to be identical to the LLFB. Scary right? Not really. Remember the commits are in two places (three if you count the
In the Right Place
Now that the personal feature branch is identical to the latest and greatest from LLFB, it’s time to bring back the commits that got blown away by the git reset. Let’s get the refs to those commits we backed up earlier. You can do a few different log commands to get those refs:
git log origin/personal-branch
Find your commits? Great! Now pass them to
cherry-pick with oldest to newest going left to right like this:
git cherry-pick --ff commit1sha commit2sha
cherry-pick applies the commits you specify one-by-one on top of the branch. You’re in! Now, your personal feature branch has the latest from the LLFB and your awesome feature applied on top! Go ahead and test things to make sure it’s all good. You will have to
git push --force now since your remote branch and your local branch commits are completely different.
The above method is great if you only have a few commits to
cherry-pick, but what if you have a bunch? Well, you can pass a “range” to git
cherry-pick like so:
git cherry-pick --ff commit1sha~1..commit10sha
This will grab commits commit1sha through
Choose the Right Tool
cherry-pick isn’t for every situation but then again neither is
merge. Git offers multiple methods to manage code, knowing when to use one or the other is a vital skill for any developer.