Getting Geeky with Git

Keeping our Git history clean with fixup commits

Marcin Wanago
Git

In this series, we’ve put a big emphasis on keeping our Git history clean. A big part of it was using features such as rebase. In this article, we go further and learn about fixup commits. With them, we can easily modify changes we’ve introduced in a single commit in our history.

Let’s create a brand new repository to visualize better a situation in which the fixup commits can come in handy.

1git init
2 
3touch index.js
4git add ./index.js
5git commit -m "Initial commit"
6 
7echo "console.log('Hell world');" > ./index.js
8git add ./index.js
9git commit -m "Added hello world"
10 
11git log
commit 675383fcbe5af72b38846e9d4b12d84d78bacee8 (HEAD -> master) Author: Marcin Wanago <wanago.marcin@gmail.com> Date: Sat Jul 31 15:10:26 2021 +0200 Added hello world commit 9a45ed3c7d689ce6db85597b0a5eaf37f3064d07 Author: Marcin Wanago <wanago.marcin@gmail.com> Date: Sat Jul 31 15:09:56 2021 +0200 Initial commit

Above, we’ve created a commit that added console.log('Hell world'); to the index.js file. Unfortunately, we’ve made a typo. Let’s fix it!

Fixing an error in a commit

One way of fixing the above issue would be to use interactive rebasing.

If you would like to know more about interactive rebasing, check out Getting geeky with Git #6. Interactive Rebase
1git rebase -i HEAD~1

When we’ve started the process of rebasing, we now need to modify our file.

1echo "console.log('Hello world');" > ./index.js
2git add ./index.js

Once we’ve fixed our error, we need to amend the commit.

1git commit --amend
2git rebase --continue
Successfully rebased and updated refs/heads/master.

Introducing fixup commits

Unfortunately, all of the above can be quite a chore. Because of that, we might be tempted to skip it and create a brand new commit that fixes our mistake. However, with fixup commits, we can do that while still maintaining a clear history!

First, let’s implement the changes we need.

1echo "console.log('Hello world');" > ./index.js
2git add ./index.js

To create a fixup commit, we need to obtain the hash of the commit we want to modify.

1git log
commit 675383fcbe5af72b38846e9d4b12d84d78bacee8 (HEAD -> master, origin/master) Author: Marcin Wanago <wanago.marcin@gmail.com> Date: Sat Jul 31 15:10:26 2021 +0200 Added hello world commit 9a45ed3c7d689ce6db85597b0a5eaf37f3064d07 Author: Marcin Wanago <wanago.marcin@gmail.com> Date: Sat Jul 31 15:09:56 2021 +0200 Initial commit

We now need to create a commit with the --fixup flag.

1git commit --fixup 675383f
Git only needs a part of the commit’s hash to identify it. If you want to know more, check out Getting geeky with Git #2. Building blocks of a commit

Doing the above creates a brand new commit with the fixup! prefix in the message and adds it to our history.

1git log
commit 1652fdd237589ca31ed7416da1c45b4158c22a8f (HEAD -> master) Author: Marcin Wanago <wanago.marcin@gmail.com> Date: Sat Jul 31 15:55:21 2021 +0200 fixup! Added hello world commit 675383fcbe5af72b38846e9d4b12d84d78bacee8 (origin/master) Author: Marcin Wanago <wanago.marcin@gmail.com> Date: Sat Jul 31 15:10:26 2021 +0200 Added hello world commit 9a45ed3c7d689ce6db85597b0a5eaf37f3064d07 Author: Marcin Wanago <wanago.marcin@gmail.com> Date: Sat Jul 31 15:09:56 2021 +0200 Initial commit

Squashing the commits

The last step in cleanup up our Git history is squashing the fix with the commit it improves. To do that, we need to perform an interactive rebase with the --autosquash flag.

1git rebase -i --autosquash HEAD~2
We can use git config rebase.autosquash true to make this a default behavior.

Doing the above squashes the original commit with the fix giving us a clean history.

The above is a simple example. The golden rule of rebasing is to avoid doing it on branches used by other developers, because it involves overwriting history and would cause issues for our teammates. Doing it on a master branch would not be a good idea in a real project.

Things to watch out for when creating fixup commits

Instead of using the git commit --fixup command, we can create a fixup commit manually.

1git commit -m "fixup! Added hello world"

Doing the above will also cause Git to recognize this as a fixup commit when rebasing with the --autosquash flag. This shows that Git uses the commit messages to figure out what commit the fixup belongs to. This can cause some issues. Let’s create a second commit with the same message:

1echo "console.log('Hell world 2')" > hello.js
2git add ./hello.js
3git commit -m "Added hello world"
4 
5git log
commit 3d10af1b5affeb5b0ad4e1d24d302ae53ee93f59 (HEAD -> master, origin/master) Author: Marcin Wanago <wanago.marcin@gmail.com> Date: Sat Jul 31 17:33:20 2021 +0200 Added hello world commit 675383fcbe5af72b38846e9d4b12d84d78bacee8 Author: Marcin Wanago <wanago.marcin@gmail.com> Date: Sat Jul 31 15:10:26 2021 +0200 Added hello world commit 9a45ed3c7d689ce6db85597b0a5eaf37f3064d07 Author: Marcin Wanago <wanago.marcin@gmail.com> Date: Sat Jul 31 15:09:56 2021 +0200 Initial commit

Above, we can see that we have two commits with the same commit message. Now, let’s create a fixup for the latest one:

1echo "console.log('Hello world 2')" > hello.js
2git add ./hello.js
3git commit --fixup 3d10af1

Doing the above results in creating a new fixup commit:

commit fe507faafd83989cf0d85e305d6001725f4db926 (HEAD -> master) Author: Marcin Wanago <wanago.marcin@gmail.com> Date: Sat Jul 31 17:39:23 2021 +0200 fixup! Added hello world commit 3d10af1b5affeb5b0ad4e1d24d302ae53ee93f59 (origin/master) Author: Marcin Wanago <wanago.marcin@gmail.com> Date: Sat Jul 31 17:33:20 2021 +0200 Added hello world commit 675383fcbe5af72b38846e9d4b12d84d78bacee8 Author: Marcin Wanago <wanago.marcin@gmail.com> Date: Sat Jul 31 15:10:26 2021 +0200 Added hello world commit 9a45ed3c7d689ce6db85597b0a5eaf37f3064d07 Author: Marcin Wanago <wanago.marcin@gmail.com> Date: Sat Jul 31 15:09:56 2021 +0200 Initial commit

Because the fixup matches more than one commit, we need to proceed with caution when rebasing.

1git rebase -i --autosquash HEAD~2

Since above, we’ve used HEAD~2, Git takes only the latest two commits into account when rebasing. Therefore, it worked out without issues.

The order of the commits when rebasing

We might encounter a problem if we rebase more commits and more than one matches the fixup commit.

1git rebase -i --autosquash HEAD~3

Fixup works similarly to squashing. It melds the fixup commit into the previous commit. Above, Git attempts to use the fixup commit for the wrong commit and gives us a conflict. To deal with it, we need to move the line with the fixup commit down.

Summary

In this article, we’ve gone through the feature of fixup commits. They can be handy when we want to alter a commit straightforwardly. The easier this process is, the less often we will take a shortcut and create an additional commit messing our history. Since the fixup and autosquash features rely on commit messages, we’ve also dealt with some issues we might encounter.