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.
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.
I fucked up Git so bad it turned into Guitar Hero pic.twitter.com/vUKZJAQKWg
— Huenry Hueffman (@HenryHoffman) February 1, 2016
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.
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.
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.
$ 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.
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.
Then I created a replacement branch for layout-tweaks
where I would apply the "rebased" changes.
$ git checkout -b better-layout-tweaks
I looked up where Wes branched off from the main
branch with git merge-base
again.
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
.
Then we can apply the diff we created with git apply
.
This will apply all the changes in the diff on the tree on the better-layout-tweaks
branch which we can then commit.
Finally we have all our changes "rebased" onto main
without having to deal with any conflicts. 🎉
Now to update the Pull Request on GitHub.
$ 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.
Rename the original feature branch beforehand if you want to keep the original as a backup.
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.