Fixing things the smart way

During development I’m constantly applying small fixes to my code. It could be anything from improving some comments, simplifying some conditional statements, or doing a small refactoring like extracting some lines into a simple method. Most of the time these small fixes end up either in small commits that dilute the git history or in commits that have a completely different focus and thus dilute the change set of those commits. But there is a better way to incorporate those kind of changes into your codebase.

You can use fix-commits to handle this kind of small fixes. So what are fix-commits and how do they work? A fix-commit is a commit that references an existing commit that it is supposed to fix or to enhance. I think this is best shown by looking at an example. Have a look at the following git history example.

Let’s say you are working on feature x and you have made some improvements to the “pre-commit conditions” done in commit 7b0ce59. It’s too late to do an amend-commit because with --amend you can only change the last commit you made. But, what you can do instead is creating a fix-commit with the following command.

git commit --fixup=7b0ce59

By doing so git will create a fixup-commit referencing a previous commit.You can see the resulting git history in the following picture. You can tell that the new commit is a fixup-commit because the commit message has the fixup! prefix and the message is automatically copied from the commit it should fix. In our case the 7b0ce59 commit.

Of course leaving it like this is not much of an improvement because now you are still diluting the git history with commits that distract you from understanding what is going on. So we have to follow up with a second step to complete cleaning up our mess. To integrate the fixes into the referenced commits, we need to perform an interactive rebase. And to really enjoy the power of fix-commits we can do the rebase with a little twist. Let’s run the following command.

git rebase -i --autosquash main

This should start the editor with the list of commits that you want to rebase. As you can see git automatically changed the order of your commits. It moved the fixup-commit to the right position directly beneath the commit it should improve. Git also changed the operation from pick to fixup which will incorporate the changes done in 26c9352 into the 7b0ce59 commit and drop the 26c9352 from the history completely.

pick 7b0ce59 Add pre-commit conditions
fixup 26c9352 fixup! Add pre-commit conditions
pick b285802 Add pre-push conditions
pick 3e4d7e4 Add commit-message conditions

The only thing left for you to do is to accept the changes by saving and exiting the editor. The resulting git history looks like this.

As you can see the fix-commit is gone, but the changes are now part of the original commit “Add pre-commit conditions”. As a side note: Be aware that the commit hashes changed because of the rebase and all caution that has to be applied to rebases in general has to be applied here as well. Do not rebased shared commits and all of that.

Depending on the size of your feature branch you don’t have to rebase all commits over and over again. You can just rebase to the furthest commit that you want to fix. You can run something like this if you only need to incorporate fixes into the last 4 commits.

git rebase -i --autosquash HEAD~4

The interactive part of it can feel a little cumbersome. Just opening the editor with -i to just save and exit is not very comfortable. Good thing that you can prevent git from opening the editor by running it like this.

git -c sequence.editor=: rebase -i --autosquash main

Not that this any more convenient but luckily we can define a git alias for this. And after defining the alias we can run git autosquash to do the rebase in a single step.

git config --global alias.autosquash "-c sequence.editor=: rebase -i --autosquash"

Besides --fixup you can use --squash as well to create fix-commits. The difference being the behavior during the rebase. While fixup will just keep the changes and get rid of the commit and its message squash will ask you to merge the commit messages manually. So if you need to change the commit message because of your fix --squash is the way to go. Be aware that you are not able to use the alias or the editor-less version when using --squash commits, because you need the editor to edit the resulting commit message manually.

With this kind of approach git add -p becomes one of your best friends as well. With partial add you can have multiple changes in the same file but handling them separately. By adding only some of the changes to the index and committing them to different fix-commits. But partial add is a different topic that we might explore in the future in a bit more detail.

As useful as fix-commits can be you should never merge or push them to your main branches. They should only exist in feature branches and only be used during feature development and code review with the clear intention to rebase them.

If you want to keep people from pushing fix-commits to branches you want to keep clear CaptainHook offers some built in functionality that will block !fixup and !squash commits from being pushed using git hooks.

Why use fix-commits?

Using fix-commits can massively improve the readability of your git log and the whole git history in general. You can use them during code review to be transparent about your changes and help reviewers to understand what kind of feedback you already incorporated and what is still open. It also makes it easier to review fixes in context with minimal change sets by only looking at the diff of the fix-commit and still be able to get rid of the commit noise afterwards. Often people squash all commits of a feature branch to get rid of all the messy commits they have made. But with this strategy you can loose some information that was present in one of the commit messages you squashed. Maybe you want to cherrypick some change to another branch. If you delete the feature branch after the successful merge of the squashed branch that possibility is gone.

There are a lot of reasons to focus on a clearer git commit strategy. It leads to more atomic commits, that leads to a more structured development approach, that leads to an easier way to implement feature flags, that leads to more robust software …

Of course I’m not saying that changing your commit strategy will solve all your problems, but it can have a positive impact on a lot of things. So maybe give it a try and get rid of all the messy and distracting “fix typo”, “add missing file”, “added comments” … commits. Your reviewers will thank you, for sure!

Leave a Reply

Your email address will not be published. Required fields are marked *