blog post image
Andrew Lock avatar

Andrew Lock

~9 min read

Smoother rebases with auto-squashing Git commits

At work the other day, a colleague mentioned they'd found my post on Git's --update-refs option for working with stacked branches, and wondered if it worked with the --autosquash option. Well, --autosquash was news to me! In this post I look at what it does, why it's useful, and how to enable it by default!

I start this post by discussing interactive rebasing in general - if you already know how and why to use interactive rebase feel free to skip ahead!

A typical interactive rebase

I'm a big fan of git rebasing. I like to work away in a branch, creating lots of tiny self-contained commits, related to different aspects. Each commit deals with a small section of the feature.

The initial set of commits

At least, that's the idea. Inevitably it's never as simple as that—there's typos or bugs to fix. That's where rebasing comes in.

Let's say that "Add base types" has a typo I want to fix. I create a new commit that fixes the typo (and only fixes that typo) and commit it.

The initial set of commits

This is probably what most people's Git workflow looks like. The difference with a rebase-focused workflow is that instead of leaving this "messy" Git history, we can tidy this up by squashing/fixing up the typo commit with "Add base types".

When you squash a commit, you merge it with its parent (previous) commit, combining the commit messages of both (and optionally changing the message). When you fixup a commit, you merge it with its parent in the same way as squash, but you just reuse the commit message of the parent as-is, without changing it. There's a variation of fixup, fixup -C that replaces the commit message with the fixup commit's message (and discards the parent commit message).

I'm a fan of doing this tidying up. It means each commit is a logical step in the implementation of the feature. I find that makes it easier to review, and essentially guides the reviewer through the changes. Rebasing is essential to create this "narrative", as you inevitably won't actually create your code in such a clean way. The alternative, just leaving the commits as they are, means reviewers have to view all the changes to your code at once, essentially only viewing the end-state, which can make it harder to review IMO.

OK, so we want to fixup our typo commit. Any time I want to squash or rearrange commits, I perform an "interactive rebase". You can initiate an interactive rebase from the command line by running:

git rebase -i main

Where main is the branch/commit that serves as the "root" of the rebase.

Only commits between the root and the tip of your current branch are included in the rebase. You can use --root to rebase your entire git tree.

When I run the above command, the default editor opens, and shows something like the following:

pick a43f263 Add initial interfaces
pick a643ac3 Add base types
pick a08d5fa Add implementation
pick 01e156a fix typo in base types

# Rebase fba8887..a08d5fa onto fba8887 (3 commands)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup [-C | -c] <commit> = like "squash" but keep only the previous
#                    commit's log message, unless -C is used, in which case
#                    keep only this commit's message; -c is same as -C but
#                    opens the editor

This lists all the potential commits for rebasing, their commit hash, and the title of the commit. You can edit this list by moving lines around and changing the pick command to something else, as described in the comments (there are more potential commands, I've truncated it for brevity!).

Note that the commits are listed from earliest to latest, so the most recent commits are shown at the bottom of the list. This is the opposite direction to the way most git graph visualizations show the most recent commits at the top of the graph.

For my example I would move the last line, commit 01e156a up one, so that it's just after the "Add base types" commit, and change the command to fixup (because I want to discard the message).

pick a43f263 Add initial interfaces
pick a643ac3 Add base types
fixup 01e156a fix typo in base types
pick a08d5fa Add implementation

Once you close the editor, Git will apply your commits, making your changes, and assuming you don't have any merge conflicts, will display something like the following:

Successfully rebased and updated refs/heads/feature.

And you can see that the typo commit has been merged into the original:

The updated set of commits are the rebase. The typo commit has been merged into the original

This is the typical command-line process for performing an interactive rebase, and I use it occasionally, but as I described in a previous post, I do most of my rebasing with Rider's GUI. I could perform the exact same interactive rebase from inside Rider.

Performing an interactive rebase with Rider

I'm a big fan of Rider's Git integration, but I spoke about it previously, so I won't go into detail here. Needless to say, you can essentially do the same things in Rider as you can from the command line, but with extra niceties like rewording commit messages in-line before you start the rebase and viewing the commit details on the right, it provides a smoother experience IMO.

That's a typical interactive rebase flow, so where does --autosquash come in, and how does it change things?

Automatic re-ordering of interactive rebase with --autosquash

When my colleague pointed out the existence of --autosquash to me, I immediately went to the git documentation. If you're already familiar with Git, the documentation is a great place to learn about new options that are introduced all the time. The docs for --autostash reveal the following:

When the commit log message begins with "squash! …​" or "fixup! …​" or "amend! …​", and there is already a commit in the todo list that matches the same ..., automatically modify the todo list of rebase -i, so that the commit marked for squashing comes right after the commit to be modified, and change the action of the moved commit from pick to squash or fixup or fixup -C respectively. A commit matches the ... if the commit subject matches, or if the ... refers to the commit’s hash.

So to summarise, --autosquash uses the following workflow:

  • You create a parent "target" commit. Let's say it has SHA 0ab12f32 and the commit message is My parent commit
  • Some time later you create a commit that you want to fixup with the target. This "fixup" commit should have one of the following commit messages:
    • fixup! My parent commit
    • fixup! 0ab12f32
  • When you subsequently run git rebase -i --autosquash, the commits will be automatically rearranged to move your fixup commit below My parent commit, and the action will be changed to fixup.

Lets explore this with the example from the previous section.

A worked example of using --autosquash

We have this initial set of commits:

The initial set of commits

We realise there's a typo in a file committed in the Add base types commit, so we make the change and prepare to create a commit. However, this time we don't choose an arbitrary commit message. Instead we use the defined format fixup! Add base types (we could also use fixup! a643ac3).

If we're working from the command line, we can generate this commit message "automatically" by passing the commit SHA of the commit we want to update and using the --fixup flag:

git add .
git commit --fixup a643ac3

This is "easier" in one sense, in that it looks up the subject (the first line of the commit message) of the target commit, and creates a commit message with the correct format:

The commit has been created with the correct message

Of course, it's not necessarily convenient to find the SHA of the target commit, but you also probably don't want to make sure you get the target subject absolutely perfect. In that case you can use a "search" to find the commit instead:

git add .
git commit --fixup ":/base types"

The :/ prefix means "try to find a commit containing the phrase base types". It will stop at the first commit it matches and use that. But be warned: the search is case-sensitive, and if the search doesn't match a commit it won't error, it will just use the provided string.

For example, if we used a search string that doesn't find any commit (in the following case I'm using the wrong case):

git commit --fixup :/Base

It will create a commit message like fixup! Base which doesn't match anything!

If this all feels a bit cumbersome for creating commit messages, I agree. In the next section I'll show how Rider makes this all a lot nicer 😉

Ok, so we have committed our fix, time to rebase. If we run

git rebase -i --autosquash main

The editor pops-open, and our commit list is pre-filled!

pick a43f263 Add initial interfaces
pick a643ac3 Add base types
fixup 48009ba fixup! Add base types
pick a08d5fa Add implementation

We can just close the editor, and the rebase is complete, without having to manually reorder any of the commits ourselves. Very nice.

Using Rider to generate --autosquash commit messages

The most painful part of this process is generating the commit message for the fixup commit as you need to know the SHA of the target commit, or be sure that you can accurately find the target commit's subject with the search string. Luckily, Rider makes this all much simpler.

Inside Rider's commit message box, if you type fixup or squash, Rider pops up a list of recent commits for you to choose from:

Rider showing a list of commits in the commit message dialog

You can simply press Enter or Tab and a commit message with the correct format is inserted; no looking up of SHAs or fuzzy matching, just the correct commit message for autosquash!

Note that Rider doesn't support the amend! prefix for doing fixup -C commits, it only supports fixup! and squash!. If you want to use amend!, I suggest typing fixup, selecting the commit from the list, and then manually changing the fixup! prefix to amend!.

This is a very handy little feature, but unfortunately Rider doesn't currently support the --autosquash flag in its rebase dialog 😢

Rider does not currently support the --autosquash flag

Luckily, there's a workaround!

Enabling --autosquash by default

Given the --autosquash only looks for "special" commit messages prefixed with fixup!, squash!, or amend!, it seems pretty safe to always add this flag. Of course, that means you need to remember to append --autosquash every time you call git rebase -i, which could be a bit tedious.

Instead, you can enable --autosquash by default in git config! Run the following command:

git config --global rebase.autosquash true

and this will update your global configuration to the following:

[rebase]
	autosquash = true

If you've chosen to enable --update-refs for working with stacked branches, as I described in a previous post, your [rebase] section will look like the following:

[rebase]
	autosquash = true
	updateRefs = true

Now you don't need to remember the --autosquash flag. Instead, if you need to disable the functionality for a specific rebase, use the --no-autosquash flag.

As well as simplifying the CLI experience, updating the git config has the advantage that it fixes Rider's interactive rebase dialog to work with autosquash too! Open up the interactive rebase dialog in Rider and you'll see all the commits rearranged and marked for fixup as you'd hope:

Rider does not currently support the --autosquash flag

I'm definitely going to start using this feature in my standard rebase workflow now, especially as Rider makes it so smooth to create the commit messages! This is one area where Rider (and I assume other IntelliJ IDEs) really do seem miles ahead of the competition (Visual Studio, Visual Studio Code)

Summary

In this post I described a common workflow I use for developing that uses Git's interactive rebase feature to move and squash commits. I then introduced the --autosquash rebase flag that removes a small amount of friction from this workflow, by automatically rearranging the commit list for you when you start a rebase. To use --autosquash, you must create your "squash" commits with a squash! or fixup! prefix, followed by the target commit's subject or SHA. Finally I showed how you can automatically use the --autosquash flag for all rebases by setting the git config value rebase.autosquash to true.

Andrew Lock | .Net Escapades
Want an email when
there's new posts?