Developers need to make things that work but we are the ones who are responsible for the code that we produce. We make a lot of decisions about that code. How it's formatted, how it is organized, how it is architected and so on. We are also responsible for how it is recorded. We use tools such as Subversion and Git and we use services like GitLab and GitHub. These tools and services often impact how we record and document our software and the changes we make. They enable us to share code across teams, review and merge changes, suggest improvements and find bugs. They are vital tools that we need to be familiar with. That is why I would like to share how I use Git rebase to help improve the history of changes I make and help you to make the most beautiful Git history that we can.

Git rebase: intimidating and interactive

Whether you're fixing bugs, migrating APIs or creating new features, I think that the capabilities of rebasing are often overlooked. Git can be intimidating. Anyone who's familiar with Git probably knows how to stage some changes, create a commit and push their changes. This is usually enough for us to start working and feel productive. Maybe you use some tools for merging and rebasing and that's great! These tools get the job done. But what happens when you open a merge request and the reviewer finds some formatting issue, suggests a better name or finds a potential bug? At Ackee, I prefer to go through each comment, fix my changes and create a commit which I can reply to the reviewer with. This strategy saves time for the reviewer, who can quickly check that the suggestion was addressed in a specific commit. I think that this is a great way to stay organized and author good merge requests. The problem is that what may have started as a single commit merge request could now have many commits. For the reviewer, it's great, but for the history of your files, it's not so great. Thankfully, some tools can help solve this issue. For example, GitLab offers an option to squash commits before merging.

I squash it all, so who cares?

Squashing an entire merge request is a great option if our merge request changes can fit nicely into a single commit and the commits that we made from the review are not valuable for the history (such as formatting or an off-by-one bug). But what happens when we make some commits which are valuable to the history of our code? One example might be that you need to modify an existing API in order to enable some new features. You start by modifying the API and then you implement the feature. Let's look at what the git history might look like in this scenario.

The situation

* C68ADDC54 (21-01-07 15:00) Implement feature A  
* 4DE8B588D (21-01-07 09:00) Extend myExampleApi() with parameters  
...older commits

We have a nice history here. You can see how myExampleApi() is changed independently from implementing feature A. It would be nice to keep these two commits in our history.

Time to open a merge request

Let's say that our reviewer finds some issues related to the changes in our first commit and has some comments related to our implementation of feature A. We will create some new commits and add them to the merge request. Now, our history will look something like this:

* 972AAEf3F (21-01-07 17:00) Split functionality into another module  
* BAF1AA554 (21-01-07 16:03) Update documentation  
* A23EE0A4E (21-01-07 16:00) Fix issue in myExampleApi() changes  
* C68ADDC54 (21-01-07 15:00) Implement feature A  
* 4DE8B588D (21-01-07 09:00) Extend myExampleApi() with parameters  
...older commits

Our reviewer agrees to the updates and allows us to merge but now our history is not really nice to look through.

Git rebase: step back

Let's take a moment to see why we want to improve the history here. Maybe we introduced a bug in our commit C68ADDC54 (15:00) Implement feature A and no one notices for another week or so. By then there are more changes to the feature. Someone could investigate the Git history while debugging and the history could look something like

* EE3AB1123 (21-01-14 12:33) Change background color  
* AF2C8DD99 (21-01-10 12:33) Update feature A  
* 972AAEf3F (21-01-07 17:00) Split functionality into another module  
* A23EE0A4E (21-01-07 16:00) Fix issue in myExampleApi() changes  
* C68ADDC54 (21-01-07 15:00) Implement feature A  
...older commits

It’s our responsibility to leave code in a better state than when we found it. Now, our colleague needs to look through each of the commits to check when the bug was introduced. Do they really care about these extra commits?

972AAEf3F (21-01-07 17:00) Split functionality into another module  
A23EE0A4E (21-01-07 16:00) Fix issue in myExampleApi() changes

I don't think so, and I would argue that they make it more difficult to look through the history (this is just an example, the history could be much longer and contain a lot more changes). It would be a lot better if we had just the one commit

* C68ADDC54 (21-01-07 15:00) Implement feature A

These changes are nice and isolated. You don't have to look through changes to myExampleApi() which are unrelated. So, let's use Git's interactive rebase to rewrite the history of our changes before we merge.

Down to the Git rebase business

First we need to identify how many commits we need to modify. The commits in our merge request currently look like this.

* 972AAEf3F (21-01-07 17:00) Split functionality into another module  
* BAF1AA554 (21-01-07 16:03) Update documentation  
* A23EE0A4E (21-01-07 16:00) Fix issue in myExampleApi() changes  
* C68ADDC54 (21-01-07 15:00) Implement feature A  
* 4DE8B588D (21-01-07 09:00) Extend myExampleApi() with parameters  
...older commits

We need to modify all five commits.

Now, before we start, I suggest that you take notice of the current commit id 972AAEf3F. Since we're rewriting history, it's nice to be able to undo things in the case that something goes wrong. You can use this commit id to reset yourself with git reflog. I suggest you check the official documentation for more about that (see Data Recovery section).

Ok, let's start. The first command will be:

git rebase -i head~5

This means that we want to interactively rebase the last five commits. At this point, whatever editor you have configured will appear with the following:

pick 4DE8B588D Extend myExampleApi() with parameters  
pick C68ADDC54 Implement feature A  
pick A23EE0A4E Fix issue in myExampleApi() changes  
pick BAF1AA554 Update documentation  
pick 972AAEf3F Split functionality into another module

You may notice that the order is reversed. The top commit is the first commit. There is also some documentation about the available commands. I suggest you read this to learn more on your own. The first thing we want to do is rearrange the order of commits. Use the editor to arrange them like this:

pick 4DE8B588D Extend myExampleApi() with parameters  
pick A23EE0A4E Fix issue in myExampleApi() changes  
pick C68ADDC54 Implement feature A  
pick BAF1AA554 Update documentation  
pick 972AAEf3F Split functionality into another module

Now, if we save and close this file, our history will be saved. But, we're not done yet! We can use the fixup command to combine these commits. Modify the file to fixup commits:

A23EE0A4E BAF1AA554 972AAEf3F.
pick 4DE8B588D Extend myExampleApi() with parameters  
fixup A23EE0A4E Fix issue in myExampleApi() changes  
pick C68ADDC54 Implement feature A  
fixup BAF1AA554 Update documentation  
fixup 972AAEf3F Split functionality into another module

Clean

And that's it! You can save and close this file and your history will be rewritten to look like our original commit history but with all the improvements from our merge request. Check the history for yourself!

* D9655EF5A (21-01-07 15:00) Implement feature A  
  
* 917574782 (21-01-07 09:00) Extend myExampleApi() with parameters  
  
...older commits

Overall, I don't think that it’s so tricky or time consuming. I think that it could even save you time in the future if you need to track down when a bug first appeared. If you have more questions or are interested in learning more I recommend reading the git-scm pages. I always find their explanations and examples to be very helpful. Good luck improving your history!

Andrew Fitzimons
Andrew Fitzimons
Android DeveloperAndrew started his programming career in embedded systems but has spent the last few years learning how to make beautiful Android apps at Ackee. Outside of work, he enjoys cooking at home, exploring Prague or hiking with his dog.

Are you interested in working together? Let’s discuss it in person!

Get in touch >