Git Dual Branches

Published Jan 06, 2018Last updated Mar 23, 2018
Git Dual Branches

Sometimes it’s hard to have fully-functioning local and production environments, either due to legacy code or poor separation of dependencies. For example: a script that works fine in production, but has never really been tested or ran locally.

In this case you may want to use separate code for local and production in order to reduce the need to refactor everything.

I’m not suggesting this is the way to go every time; just in those very particular situations where it’s better to err on the side of caution (by testing locally) without creating excess work.

Git setup

Using git you’ll need a sort of pseudo branch that has code only for your local environment. This pseudo branch needs to allow for both local-only code changes, as well as for the bug or feature you are working on.

So, right away we can envision three separate branches:

  1. The local changes required to work on any feature or bug
  2. The specific feature or bug you are working on
  3. A combination of #1 and #2 based off of #1

A really rough visual might look like this quick sketch I made in the Apple Notes app:

Sketch of three git branches

Branches 1 and 2 sync up with their remote origins because they contain uniquely independent code. Branch 3 syncs with 1 and 2 locally via cherry-pick (more on that in a minute) because it can only run locally. Branch 3 might have it’s own remote origin also, but it is never production (upstream)-ready code since it always has local-only changes. The only reason to push branch 3 is to retain it’s working state which is useful if you want to avoid repeat stashing and let others have visibility to your local code changes.

Branch 3 is essentially a garbage or sandbox branch, in that it will never be merged into upstream/production.

In order to get changes into their respective branches, you commit in branch 3 then cherry-pick into the other branches. This way branch 3 always has the fully functional code for the local environment, and branches 1 and 2 only contain commits relevant to each branch’s purpose. Branches 1 and 2 don’t execute locally; they exist solely as a bridge to be merged into upstream-ready code.

You can also cherry-pick from branches 1 and 2 into 3. This often happens when you need to make a minor change to the upstream-ready code that doesn’t need to be tested locally. An example being setting a timeout value higher, or adding a code comment. It’s often quicker to just make the change in the production-ready branch first, push, and continue collaborating with others, than to perform four steps every time: commit, switch, cherry-pick, push.

Cherry-picking versus merging

Merging will bring all changes from one branch into another. We absolutely do not want this to happen from branch 3 into branches 1 or 2 because branches 1 and 2 are completely independent feature or bugfix-related code. They don’t know or care about each other.

The only option is to cherry-pick individual commits from branch 3 into branches 1 and 2. Cherry-picks will create new commit hashes, which can cause weird issues down the road if there are multiple commits with the same exact changes but different hashes (and possibly authors!) but that’s not a concern because branch 3 is never merged into upstream.

More on stashing

Stashing is quick and handy, but over time your stashes will get out of control. If you have more than a few stashes, it’s easy to lose track of the purpose and branch behind each stash. I know there are creative ways to stash whereby you can organize and execute stashes more clearly, but in practice I’ve found stashing to be messy and only useful in emergencies.

The other main drawback of stashing is the inability to push stashes to remotes. If your computer dies, your stashes die with it. I don’t like that I have a bunch of unfinished and undocumented work sitting solely on my machine.

Philosophically, I’ve learned over time that I stash for really only one reason: I need to switch branches and I don’t have time to review and commit everything. The changes could be actual bug fix or features that haven’t been completely fleshed-out, or just plain debugging code.

Not having time to commit is a poor excuse to use a feature (stashes) that was most likely designed for this exact purpose: the ability to quickly switch branches without stopping to commit what you have. In this manner, stashing enables sloppy developer practices.

The benefit of a dedicated sandbox branch (#3) is that it’s a true branch with commit history even for the smallest or least-likely of changes that will make it to upstream.

The process matters.

Stop thinking of your local changes as only being temporarily important or relevant locally. Your entire coding process (building and debugging) matters, and if that’s not visible or evident in commits to others, you’re not growing as a developer.

Final thoughts

If you are not already, get used to making partial file commits (in branch #3). Through the command line this is done with git add -p <filename>. Quick primer: use y + Enter to stage a particular chunk of code, and j + Enter to ignore it. That’s it!

And because I like to get down into details, here is a more comprehensive drawing of how the branches would be set up. Below illustrates a bug fix and a feature both using dual branches.

I apologize for the crudeness here (there are a lot of arrows being thrown at you):

Full sketch of git dual branches

(I also toyed with titling this post “Git Multi-Branches or “Git Three-Tiered Projects.”)

Discover and read more posts from Matt Thommes
get started