Developer Blog

High-velocity Software Development Using Nested Git Branches

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. Is 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 multitasking and 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) to make this type of working easy and efficient.

Example

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: – clean up some technical drift by improving the names of classes and functions that don’t make sense anymore – add some flexibility to the architecture so that the new feature can be built with less hackery – while looking through the code base, we also found a few typos we want to fix

Let’s implement these things as a chain of independent but connected feature branches! The tables below shows the Git Town commands – as well as the individual Git commands you would have to run without Git Town – to achieve that. First, let’s fix those typos because that’s the easiest change and there is no reason to keep looking at them.

Fix typos

We create a feature branch named 1-fix-typos to contain the typo fixes from the master branch:

Git Town command Git commands description
git hack 1-fix-typos git checkout master we want to build on top of
the latest version of master
git pull
git checkout -b 1-fix-typos

We do a few commits fixing typos, and submit a pull request:

1
git new-pull-request

This opens a browser window to create the pull request on your code hosting service. All of this only took us 30 seconds. While the code review for those change starts taking place, we move on to fix the technical drift.

Rename 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 Town command Git commands
git append 2-rename-foo git checkout -b 2-rename-foo

git append creates a new feature branch by cutting it from the current branch, resulting in this branch hierarchy:

1
2
3
4
5
master
  \
   1-fix-typos
     \
      2-rename-foo

Now we commit the changes that rename the foo variable and start the next pull request:

1
git new-pull-request

Because we use 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 only shows changes made in branch 2, but not the changes made in branch 1.

Rename 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 at the same time. So let’s make these changes on top of the changes we made in step 2:

Git Town command Git commands
git append 3-rename-bar git checkout -b 3-rename-bar

We end up with this branch hierarchy:

1
2
3
4
5
6
7
master
  \
   1-fix-typos
     \
      2-rename-foo
        \
         3-rename-bar

Fix more typos

While renaming bar, we stumbled on a few more typos. Let’s add them to the first branch.

1
2
3
git checkout 1-fix-typos
# make the changes and commit them here
git 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 Town command Git commands
git sync git checkout -b 2-rename-foo
git merge 1-fix-typos
git push
git checkout 3-rename-bar
git merge 2-rename-foo
git push

Generalize the infrastructure

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 Town command Git commands
git append 4-generalize-infrastructure 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.

Shipping the typo fixes

In the meantime, we got the approval for the typo fixes in step 1. Let’s ship them!

Git Town command Git commands description
git ship 1-fix-typos git stash -u move things we are currently working on out of the way
git checkout master update master so that we ship our changes on top of the most current state of the code base
git pull
git checkout 1-fix-typos
git pull make sure the local machine has all the changes made in the 1-fix-typos branch
git merge master resolve any merge conflicts between our feature and the latest master now, on the feature branch
git checkout master
git merge –squash 1-fix-typos use a squash merge to get rid of all temporary commits and merges on the branch
git push
git branch -d 1-fix-typo delete the branch from the local machine
git push origin :1-fix-typo delete the branch from the remote repository
git checkout 4-generalize-infrastructure return to the branch we were working on
git stash pop restore what we were working on

With branch 1-fix-typos shipped, our branch hierarchy looks like this now:

1
2
3
4
5
6
7
master
  \
   2-rename-foo
     \
      3-rename-bar
        \
         4-generalize-infrastructure

Synchronizing our work with the rest of the world

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 the master branch, since that can lead to more severe merge conflicts later. Let’s get everything in sync!

Git Town command Git commands description
git sync git stash -u move things we currently work on out of the way
git checkout master
git pull
git checkout 2-rename-foo
git merge master
git push
git checkout 3-rename-bar
git merge 2-rename-foo
git push
git checkout 4-generalize-infrastructure
git merge 3-rename-bar
git push
git 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.

Build the new feature

Back to business. With the new generalized infrastructure 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 Town command Git commands
git append 5-add-feature git checkout -b 5-add-feature

Let’s stop here. Hopefully, it is clear how to work more focused using Git Town. Let’s review: – each change happens in its own feature branch – run git append to create a new feature branch – run git sync to keep all feature branches in sync with the rest of the world – do this several times a day – run git ship to ship a branch once it is approved

Advantages of focused feature branches

Working this way has a number of important advantages:

  • Focused changes are easier and faster to create: if you change just one thing, you can do it quickly, make sure it makes sense, and move on to the next issue in another branch. No more getting stuck not knowing which of the many changes you did in the last 10 minutes broke the build, and no need to fire up the debugger to resolve this mess.
  • They are easier and faster to review: The PR can have a simple description to summarize it. Reviewers can easily wrap their heads around what the PR is doing and make sure the changes are correct and complete. This is also true if you write code just by yourself. You should give it a final review and cleanup before merging it into master!
  • Branches containing focused changes cause less merge conflicts than branches with many changes in them. They make fewer changes, and the changes are typically more distributed across lines. This gives Git more opportunity to resolve merge issues automatically.
  • In case you have to resolve merge conflicts manually, they are also easier and safer to resolve. You know exactly what single change both branches perform, versus what five changes each of the branches perform in the unfocused scenario.
  • Others can start reviewing parts of your changes sooner because you start submitting pull requests earlier.

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.

Best Practices

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 (I use 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, create the next branch, perform the new changes there, then return to the previous branch and finish the work there. If you somehow ended up doing several changes at once in a branch anyway, you can still refactor your Git branches so that they end up containing only one change.

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:

1
2
git checkout 2-rename-foo
git prepend 1-other-large-refactor

This leads to the following branch hierarchy:

1
2
3
4
5
6
7
8
9
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!

Comments