Slice, Dice, and Squash Your Git Commit History
Anthony Fung
Posted on March 22, 2023
Release notes are an important part of releasing software. It doesn’t matter if they are available publicly, internally within a company, or just to yourself: they help to track changes made in each new version. I once worked at a company where we had a two-week release cycle. Each one was accompanied by a set of release notes that were put together manually. As these took a long time to write, we wanted to automate creating them – or at least to simplify the process.
Having switched over to Git, we wanted to use commit messages to build a first draft. At that point, we didn’t have a template for commit messages – we just entered a short summary of what had changed. We also used Jira to track features and bugs, so we introduced a loose template to group related commits together: ticket ID, followed by a short description.
This helped, but there was still one problem.
Complex features often consist of many development milestones. Committing after each one is a good idea: if the next milestone is difficult, we may want to try several different approaches. By creating a commit, we create a safe place to reset the code to so that we can try again. It’s also good practice for backup purposes.
When we adopted the commit message template, we found that having multiple milestone commits for each feature complicated things. We now had multiple commits with the same ticket ID, but different description texts. As they described the code changes, it was difficult to say what the overall feature or bugfix was. To solve this, we considered squashing all related commits for a feature into a single commit before pushing.
Once a feature was complete, we found it quite rare to need to step back-and-forth through the development milestones, so we agreed to do this. Features often required tweaking and fixing after testing. Those adjustments were detailed in separate tickets, so that didn’t cause duplicates in the release notes.
So how do we squash multiple commits into one?
Changing History
Once written, a repository’s commit history can be modified. Let’s look at an example in Git Extensions. We have three commits on a branch (called feature). Development is complete and we want to squash the following commits into a single commit:
- Feature commit 3
- Feature commit 2
- Feature commit 1
We first need to count how many commits we want to affect: three in this case. We can then enter the following into a console:
git rebase -i HEAD~3
The number at the end of the command, 3
, is the number that we previously counted. Alternatively, we could also specify the hash of the commit we want to rebase onto – this would be the one before the first commit that we want to include. In this case, it would be d679938
:
git rebase -i d679938
Entering either of these commands will launch the default text editor that Git has been configured to use. This is Visual Studio Code in this example.
Image 2 shows the commits included in the interactive rebase. All three commits that we wanted to include are listed, and we can see that they are all prefixed with pick
. Feature commit 1 (ab5d5c8
) is the first commit for our feature, so we leave this as pick
. However, we want to change the commands for the remaining commits (ff2b752
and 0be8e98
) to s
, or squash
; either will do. As the help text describes, this will include the commit’s code changes, but meld them into the previous commit.
Image 3 shows the same list with the commands for commits ff2b752
and 0be8e98
set to s
(for squash
). If there are any commits that shouldn’t be squashed, leave the command as pick
.
It’s important to note the order of the commits. The oldest commit shown in Image 3 is at the top of the list; the most recent is shown at the bottom. The order is reversed from how they are shown in Git Extensions, where the newest commit is show at the top of the graph. We can close the editor when we are happy with the commands for each commit. After a short while Git will launch the text editor again with a chance to edit the commit message.
We can see in Image 4 that we have a chance to edit the commit message for the new commit that will be created. The messages of all commits in the squash are shown. If we left this as is, our commit message would consist of all three lines of text. For our example, we want to change it to Best feature yet. This is shown in Image 5.
After saving and closing the editor, the rebase will continue. In our example, there is nothing left to do and so the rebase completes. Image 6 shows the result of the interactive rebase in Git Extensions. We can see that the previous commits (Feature commit 1, Feature commit 2, and Feature commit 3) have been combined to form a new commit: Best feature yet.
Note also that the date and author of the commit remain the same.
Summary
A branch’s commit history can be modified. If you ever want or need to combine multiple commits into one, you can do so with an interactive rebase. First, choose the point that you’d like the rebase to start at. You can then choose what happens with each commit. Among the options, you can squash commits into the previous ones, or you can leave them as they are. After closing the editor, you can edit the commit messages for the newly combined commits.
It’s a good idea to make frequent work-in-progress commits. They’re great ‘restore points’ to fall back on if you ever need to while trying something new. While they can clog up your commit history, you no longer have to worry about this. With an interactive rebase, you can combine many related commits into one. And as you’ll be able to write a single commit message to describe the overall change, you’ll be the envy of others who want to know your secret of making such clean commits.
Thanks for reading!
Level up your developer skills! Sign up for free for more articles like this, delivered straight to your inbox (link goes to Substack).
Posted on March 22, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.