Implementing several different unrelated code changes at the same time in the same feature branch is like trying to have a conversation about several completely different topics at the same time with the same person, or other forms of multi-tasking. It’s never productive. We end up mixing up issues or forgetting to think about important edge cases of one topic because we are distracted with the other topics. We only have parts of our brain available to think about each topic. Dealing with several issues at the same time might create the illusion of productivity, but in the end, it is faster, easier, safer, cleaner, and less error-prone to take on each topic separately.
This blog post describes a technique for highly focused development by implementing code changes using a series of nested Git branches. We use specialized tooling (Git Town, an open-source plugin for Git) to make this type of working easy and efficient.
As an example, let’s say we want to implement a new feature for an existing product. But to make such a complex change, we have to get the code base ready for it first:
Let’s implement these things as a chain of independent but connected feature branches! I provide the Git Town commands as well as the individual Git commands you would have to run without Git Town for those unfamiliar with that tool. First, let’s fix those typos because that’s the easiest change and there is no reason to keep looking at them.
We create a feature branch named 1-fix-typos
to contain the typo fixes from the master
branch:
git hack 1-fix-typos
git hack is a Git command added by Git Town. The corresponding vanilla Git commands are:
git checkout mastergit pullgit checkout -b 1-fix-typos
We always want to build new changes on top of the latest version of the master branch.
We do a few commits fixing typos and submit a pull request:
git new-pull-request
This Git Town command opens a browser window to create the pull request on your code hosting service.
All of this only took us under a minute. While the code review for those change happens, we move on to fix the technical drift.
foo
We don’t want to look at the typos we just fixed again, so let’s perform any further changes on top of branch 1-fix-typos
:
git append 2-rename-foo
git append
creates a new feature branch by cutting it from the current branch, resulting in this branch hierarchy:
master \ 1-fix-typos \ 2-rename-foo
The corresponding vanilla Git commands are:
git checkout -b 2-rename-foo
Now we commit the changes that rename the foo
variable and start the next pull request:
git new-pull-request
Because we used git append
to create the new branch, Git Town knows about the branch hierarchy and creates a pull request from branch 2-rename-foo
against branch 1-fix-typos
. This guarantees that the pull request for branch 2 only shows changes made in that branch (renaming the variable), but not the syntax fixes made in branch 1.
bar
This is a different change than renaming foo
, so let's do it in a different branch. Some of these changes might happen in the same places where we just renamed foo. We don't want to have to deal with merge conflicts later. Those are boring and risky. So let's make these changes on top of the changes we made in step 2:
git append 3-rename-bar
We end up with this branch hierarchy:
master \ 1-fix-typos \ 2-rename-foo \ 3-rename-bar
The corresponding vanilla Git command is
git checkout -b 3-rename-bar
While renaming bar
, we stumbled on a few more typos. Let's add them to the first branch.
git checkout 1-fix-typos# make the changes and commit them heregit checkout 3-rename-bar
Back on branch 3-rename-bar
, the freshly fixed typos are visible again because the commit to fix them only exists in branch 1-fix-typos
right now. Luckily, Git Town can propagate these changes through all other branches automatically:
git sync
The corresponding vanilla Git commands are:
git checkout -b 2-rename-foogit merge 1-fix-typosgit pushgit checkout 3-rename-bargit merge 2-rename-foogit push
Okay, where were we? Right! With things properly named it is now easier to make sense of larger changes. We cut branch 4-generalize-infrastructure
and perform the refactor in it. It has to be a child branch of 3-rename-bar
, since the improved variable naming done before will make the larger changes we are about to do now more intuitive.
git append 4-generalize-infrastructure
Again, in vanilla Git:
git checkout -b 4-generalize-infrastructure
Lots of coding and committing into this branch to generalize things. Since that’s all we do here and nothing else, it’s pretty straightforward to get through it, though. Off goes the code review for those changes.
In the meantime, we got the approval for the typo fixes in step 1. Let’s ship them!
git ship 1-fix-typos
The vanilla Git commands:
git stash -u # move open changes out of the waygit checkout master # update master so that we ship our changes# on top of the most current changesgit pullgit checkout 1-fix-typos # make sure the local machine# has all the changes made in the# 1-fix-typos branchgit pullgit merge master # resolve any merge conflicts# between our feature and the latest master now,# on the feature branchgit checkout mastergit merge — squash 1-fix-typos # use a squash merge# to remove all temporary commits# on the branchgit push # make our shipped feature visible to# all other developersgit branch -d 1-fix-typo # delete the shipped branch# from the local machinegit push origin :1-fix-typo # delete the shipped branch# from the remote repositorygit checkout 4-generalize-infrastructure # return to the branch# we were working ongit stash pop # restore open changes we were working on
With branch 1-fix-typos
shipped, our branch hierarchy now looks like this:
master \ 2-rename-foo \ 3-rename-bar \ 4-generalize-infrastructure
We have been at it for a while. Other developers on the team have shipped things too, and technically the branch 2-rename-foo
still points to the previous commit on master. We don't want our branches to deviate too much from what’s happening on the master branch since that can lead to more severe merge conflicts later. Let's get everything in sync!
git sync
The corresponding vanilla Git commands are:
git stash -u # move open changes out of the waygit checkout mastergit pullgit checkout 2-rename-foogit merge mastergit pushgit checkout 3-rename-bargit merge 2-rename-foogit pushgit checkout 4-generalize-infrastructuregit merge 3-rename-bargit pushgit stash pop # restore what we were working on
Because we used git append
to create the new branches, Git Town knows which branch is a child of which other branch, and can do the merges in the right order.
Back to business. With the new generalized code architecture in place, we can now add the new feature in a clean way. To build the new feature on top of the new infrastructure:
git append 5-add-feature
Let’s stop here. Hopefully, it is clear how Git Town allows to work in several Git branches in parallel. Let’s review:
git append
creates a new feature branch on top of your existing workgit sync
keeps all feature branches in sync with the rest of the world - do this several times a daygit ship
ships a feature branchWorking this way has a number of important advantages:
Ultimately, using this technique you will get more work done faster. You have more fun because there is a lot less getting stuck, spinning wheels, and starting over. Working this way requires running a lot more Git commands, but with Git Town this is a complete non-issue since it automates this repetition for you.
To fully leverage this technique, all you have to do is follow a few simple rules:
postpone ideas: when you work on something and run across an idea for another change, resist the urge to do it right away. Instead, write down the change you want to do (on a sheet of paper or a simple text file), finish the change you are working on right now, and then perform the new change in a new branch a few minutes later. If you can’t wait at all, commit your open changes into the current branch, create the next branch, perform the new changes there, then return to the previous branch and finish the work there.
go with one chain of branches: When in doubt whether changes depend on previous changes and might or might not cause merge conflicts later, just work in child branches. It has almost no side effects, except that you have to ship the ancestor branches first. If your branches are focused, you will get very fast reviews, be able to ship them quickly, and they won’t accumulate.
do large refactorings first: In our example, we did the refactor relatively late in the chain because it wasn’t that substantial. Large refactorings that touch a lot of files have the biggest potential for merge conflicts with changes from other people, though. You don’t want them hanging around for too long, but get them shipped as quickly as you can. You can use git prepend to insert feature branches before the currently checked out feature branch. If you already have a long chain of unreviewed feature branches, try to insert the large refactor into the beginning of your chain, so that it can be reviewed and shipped as quickly as possible:
git checkout 2-rename-foogit prepend 1-other-large-refactor
This leads to the following branch hierarchy:
master \ 1-other-large-refactor \ 2-rename-foo \ 3-rename-bar \ 4-generalize-infrastructure
The new large refactor is at the front of the line, can be shipped right when it is reviewed, and our other changes are now built on top of it.
Happy hacking!