appsignal

Rebasing a complex branch in Git

Tom de Bruijn

Tom de Bruijn on

Rebasing a complex branch in Git

A while ago I volunteered to rebase a very complex branch on the AppSignal frontend. In this post I'll describe how I rebased the branch internally known as layout-tweaks, but you might know it as the new AppSignal design.

A couple weeks into working for AppSignal, the issue came up about rebasing a complex branch. Wes, AppSignal's designer, asked if someone could help out rebasing the layout-tweaks branch. Confident in my rebasing skills I volunteered.

Doubt first kicked in when I opened the Pull Request's commit tab on GitHub. There were 93 commits created over the span of two months. The changes included almost all Rails view templates, all CSS stylesheets and quite a few JavaScript files.

It was quite clear why my co-workers had some trouble rebasing this branch.

The standard rebase

My first attempt was optimistically naive. I tried to rebase the layout-tweaks branch onto the latest version of the main branch to get it up-to-date with all the latest changes.

sh
$ git checkout main $ git pull origin main # get new changes $ git checkout layout-tweaks $ git rebase main

If the changes between main and your feature branch are unrelated git happily rebases your feature branch for you.

If there are changes that overlap between main and your feature branch, git will prompt you to resolve the conflicts before it can continue.

During my rebase of the layout-tweaks branch I had a lot of conflicts to resolve. After a couple commits fixing the same issues over and over again I tried another approach.

Squashing commits

Wes designs in the browser and commits often to keep track of his changes. Committing often is a good habit for anyone who uses git. However, lots of commits will also increase the chance of more merge conflicts.

Squashing commits can help with reducing the amount of merge conflicts.

I prefer to rebase and squash on the commit a feature branch was started from, the branch off point. This way you can rebase and squash before you have to resolve conflicts against new changes on the main branch.

When your git history looks a bit like in the tweet above, it can be a bit difficult to find the original branch off point. Luckily we can ask git with the git merge-base tool.

sh
$ git merge-base main layout-tweaks # 945369734b4ca9fcee6cb88e6283fb7f9c52b304

Git returns the commit SHA of the commit the branch was most likely started on. We can use this commit SHA to rebase on instead.

sh
$ git checkout layout-tweaks $ git rebase -i 945369734b4ca9fcee6cb88e6283fb7f9c52b304 # Squash commits into 1/a few

Now that we're able to only rebase the changes in our own feature branch, it's a lot easier to squash commits.

With fewer commits and less potential conflicts we can then rebase on the changes in the main branch.

shell
$ git rebase main

Normally, this strategy is a nice way to save yourself a headache by squashing first and rebasing on new changes after.

Merge commits, oh no…

When I rebased and squashed the layout-tweaks branch, something interesting happened; I still ran into a lot of conflicts.

The main branch was already merged into the layout-tweaks branch 4 times with a merge commit. You'll recognize these by their names.

shell
Merge branch 'main' into layout-tweaks

When you merge the main branch into a feature branch with a merge commit, you also bring in the main branch's new commits. This way we're actually rebasing more commits than only those on your feature branch.

For layout-tweaks it jumped from 93 commits to more than 200.

With so many commits and merges, it no longer seemed possible to rebase this branch without manually resolving all the conflicts. The chance that I would make a mistake in the process was too big.

The rebase that's not a rebase

Looking at the Pull Request on GitHub I was greeted with a big green merge button. The merge commits actually made it possible to merge the layout-tweaks branch into main. Wes already fixed most of the conflicts when he merged main into layout-tweaks those 4 times.

I could have just given up and not rebased. The Pull Request wasn't done yet and we could continue fixing conflicts with more merge commits. Who cares about a bit of chaos in the git history?

I do.

Knowing the Pull Request was mergable without conflicts actually gave me an idea.

I took the "diff" between the two branches and simply applied that to the main branch.

The result is the same as a rebase, except this approach puts all the changes in one commit.

Applying a diff

First I switched to the main branch and pulled in the latest changes.

sh
$ git checkout main $ git pull origin main # get new changes

Then I created a replacement branch for layout-tweaks where I would apply the "rebased" changes.

shell
$ git checkout -b better-layout-tweaks

I looked up where Wes branched off from the main branch with git merge-base again.

sh
$ git merge-base main layout-tweaks # 945369734b4ca9fcee6cb88e6283fb7f9c52b304

Using the commit SHA and the main branch git reference, we can ask git for the complete diff between the two points and save that to a file named diff.

sh
$ git diff --no-color 945369734b4ca9fcee6cb88e6283fb7f9c52b304 layout-tweaks > diff

Then we can apply the diff we created with git apply.

sh
$ git apply diff $ rm diff # and clean up after ourselves

This will apply all the changes in the diff on the tree on the better-layout-tweaks branch which we can then commit.

sh
$ git add . $ git commit -m "Tweak layout" --author="Author Name <email@address.com>"

Finally we have all our changes "rebased" onto main without having to deal with any conflicts. 🎉

Now to update the Pull Request on GitHub.

shell
$ git push origin better-layout-tweaks:layout-tweaks --force-with-lease

(About why you should use --force-with-lease I refer you to this great article on thoughtbot's blog.)

Cleaning up

Now that we have a clean new feature branch we can remove the old one and overwrite its history.

sh
$ git branch -D layout-tweaks # destructive deletion! $ git checkout -b layout-tweaks # create a new branch from better-layout-tweaks $ git branch -D better-layout-tweaks # destructive deletion!

Rename the original feature branch beforehand if you want to keep the original as a backup.

sh
$ git branch -m layout-tweaks old-layout-tweaks

Conclusion

In the end we had a neatly "rebased" history on the layout-tweaks Pull Request. From there we could continue the redesign.

Some development process history was lost since we squashed the commits, but nothing that couldn't be documented later. It's always possible to split commits afterwards if one commit just won't do.

Hopefully my journey gives you more insight into rebasing and some strategies on how to minimize the amount of merge conflicts you have to resolve.

If you don't rebase a lot because you heard about or experienced loss in data, just know it's very difficult to actually lose your data forever in git. There's always git reflog.

It certainly was a lot of fun for me rebasing such a complex branch onto the AppSignal project.

Also, to prevent this kind of branch complexity in the future, we're now making sure to keep branches closer to their origins by merging smaller chunks of code and rebasing our feature branches more often.

Tom de Bruijn

Tom de Bruijn

Tom is a developer at AppSignal, organizer, and writer from Amsterdam, The Netherlands.

All articles by Tom de Bruijn

Become our next author!

Find out more

AppSignal monitors your apps

AppSignal provides insights for Ruby, Rails, Elixir, Phoenix, Node.js, Express and many other frameworks and libraries. We are located in beautiful Amsterdam. We love stroopwafels. If you do too, let us know. We might send you some!

Discover AppSignal
AppSignal monitors your apps