git commit: fixup and squash automatically

2020-08-16

 | 

~5 min read

 | 

873 words

Of late, I’ve been getting more and more interested in learning about how to write better commits. This has led me to discover not only how to write more meaningful messages, but also how to tell a story with my commit logs - both for archeological purposes, but also to tell a story. The latter is something I’m working on through intentional rebasing. It’s also this latter point that led me to discover two options of the Git’s commit command which is the topic of today’s discussion: fixup and squash. Using these options may obviate the need to use an interactive rebase by declaring up front which commits should be squashed or combined.1 At a minimum, they can make a rebase simpler by putting in some work up front.

How The Commands Work

Imagine you noticed an error in your code that’s part of the feature you’re working on. All of the code is still private (i.e. it’s not been pushed up to be shared / used by others). At this point, you have a few common options to fixing it:

  1. Fix it with a new commit and call it a day. This is the simplest approach, but it muddies the commit history.
  2. Fix it with a new commit and then use an (interactive) rebase to move the commits around so that the commit history tells a clean narrative, or even better, is squashed into the original commit as a single point in the story.
  3. Fix it with a new commit and use the --fixup or --squash option to make the future rebase even easier.

It’s the third option which is the focus of this post.

Definitions

First, what do these options actually do? From the Git manual:

--fixup=<commit>

Construct a commit message for use with rebase —autosquash. The commit message will be the subject line from the specified commit with a prefix of “fixup! “. See git-rebase[1] for details.

--squash=<commit>

Construct a commit message for use with rebase —autosquash. The commit message subject line is taken from the specified commit with a prefix of “squash! “. Can be used with additional commit message options (-m/-c/-C/-F). See git-rebase[1] for details.

(As a reminder, the difference between a fixup and a squash has to do with how commit messages are handled.)

Seeing The Commands In Action

Now that we have a decent understanding of what these commands can do and why we might want to use them, let’s look at an example.

The current feature we’re implementing is related to authentication. Imagine the following commit history for a project:

git log --pretty=format:'%h | %s%d [%an]' --graph
* a384e6b | feat: reset password (HEAD -> master, origin/master, origin/HEAD) [Stephen]
* 2f76abe | feat: set password [Stephen]
* 4c0eb1b | feat: set email [Stephen]
* ...

Preparing to submit a change set for review, we notice that there are a few problems with our set password feature. We make a few changes and are now ready to commit the updated code. In this case it’s just a fixup, so we could do either:

$ git commit --fixup 2f76abe
# or
$ git commit --fixup :/"set password"

Now, when we print the commit history, we see a new fixup! commit added automatically.

git log --pretty=format:'%h | %s%d [%an]' --graph
* 766d9c2 | fixup!: feat: set password (HEAD -> master, ) [Stephen]
* a384e6b | feat: reset password (origin/master, origin/HEAD) [Stephen]
* 2f76abe | feat: set password [Stephen]
* 4c0eb1b | feat: set email [Stephen]
* ...

The :/ syntax in the second option is a string search that Git interprets as “the most recent commit that contained the string foo in the first line of it’s commit message”1

This hasn’t actually achieved our goal of cleaning up the commit history (yet). So there’s one more step: rebasing.

Note that both fixup and squash are designed to be used with the autosquash option of a rebase. (If this option is not included, the commits will be treated just like a normal commit during a rebase.) By using the autosquash option with the rebase command Git will rearrange the commits for us and preset the rebase commands for us.

For example:

git rebase --interactive 4c0eb1b --autosquash
pick 2f76abe 📝 cancelable fetch
fixup 766d9c2 fixup! 📝 cancelable fetch
pick a384e6b 📝 draft of volta

Conclusion

It’s easy to get comfortable with a certain way of doing things, but I find that when I ask questions about why something works one way, I invariably find a better way. That’s certainly been my experience with Git, which is a reason why I write about it so frequently. It’s an important tool for how I write software and I’d be derelict in my duties if I didn’t constantly push to find new ways to take advantage of it to be more efficient.

Footnotes

  • 1 It may make the interactive rebase unnecessary, though of course there are a lot of other features available within a rebase beyond just fixup and squash.
  • 1 Thank you to Thoughtbot for this technique and interpretation!


Hi there and thanks for reading! My name's Stephen. I live in Chicago with my wife, Kate, and dog, Finn. Want more? See about and get in touch!