blog post image
Andrew Lock avatar

Andrew Lock

~9 min read

Super-charging 'git rebase' with 'git absorb'

In this post I look at the git-absorb tool for automatically creating fixup! commits. This is a follow on to my previous post on using --autosquash, in response to a comment mentioning it. I wasn't aware of the git-absorb tool at all, so in this post I look at what it does, how to install it, and how to configure it!

What does git-absorb do?

git-absorb is a "plugin" for Git that can automatically create fixup! commits for use with git rebase -i --autosquash. It's a port of Facebook's hg absorb tool. In this section I describe when it's useful.

In my previous post, I described how you can use the --autosquash option of Git rebase to automatically rearrange your commits when performing an interactive rebase.

When creating a "fixup" commit that you know you will want to squash later, you use a special fixup! or squash! prefix, followed by the original commit's message, and then git rebase --autosquash will automatically rearrange the commits for you:

pick a43f263 Add initial interfaces
pick a643ac3 Add base types
fixup 48009ba fixup! Add base types # 👈 Automatically moved thanks to austosquash
pick a08d5fa Add implementation

Rider really helps with generating these messages too, by giving you a drop down list of commits to fixup:

Rider showing a list of commits in the commit message dialog

This workflow takes a little bit of the manual work out of doing an interactive git rebase. Instead of having to remember which commits need to be squashed where at rebase-time, you define it at commit-time, and let --autosquash sort it out. It's a nice little improvement.

What this --autosquash workflow doesn't help with, is splitting up that big list of uncommitted files into the small units necessary to create all the fixup commits. And that's a lot of work 😩

That's where git-absorb comes in.

git-absorb is a magical little tool that is almost too good to be believed. It looks through your uncommitted changes, and works out the sequence of "fixup" commits that are necessary to "absorb" your changes into previous commits.

That's right, it does all the hard work for you 🤯

Let's look at a quick example.

Imagine I'm working on a feature and I've already done most of the work:

The initial set of commits

During my testing, I discover some bugs and make edits to various files. Now, I could manually work out which of the files need to be fixed-up into each of the previous commits, but that could be an annoying amount of work.

So instead, I can use git-absorb To figure it out for me!

git-absorb only operates on files that have been added to the Git staging area, so you first need to run git add . to add the files you want to work with. You can use this feature to ensure git-absorb only generates fixup commits for a subset of the modified files.

git add . # Include all the files in fixup commits. Alternatively can include a subset.
git absorb # Generate fixup commits using the default settings

If everything goes to plan, you should see output similar to the following, which lists the commits that have been created.

INFO committed, header: -1,1 +1,1, commit: 5395eafdbb2740dc76adb234b112914b491ffd44
INFO committed, header: -1,1 +1,1, commit: eacb60be2235b621f50158c68ae7a5e61a313081
INFO committed, header: -0,0 +1,1, commit: f5c5748a20bd6fa23b563844c1cddeeb683f1499

If you look at the git tree, you'll see that each of these commits has a fixup! commit which you can use with git rebase -i --autosquash. This automatically moves the commits to their correct location - all you need to do is hit "go"!

Image of Rider showing the auto-squashed commits

As far as I can see, git absorb produces at least one fixup commit per file. So if you have 10 edited files staged in your git index, you'll have at least 10 fixup commits.

Note that if you've introduced any new files, git-absorb will ignore them entirely, as it has no way of knowing which commit they should be absorbed into. But all the other changes are "absorbed" into a previous commit. It takes 90% of the manual process out of it! 🎉

How does git-absorb work behind the scenes?

git-absorb is a simple command-line utility (built with Rust) that you make available in your path (alternatively you can use a Git alias, as I'll show in the next section). According the documentation:

git absorb works by checking if two patches P1 and P2 commute, that is, if applying P1 before P2 gives the same result as applying P2 before P1.

Judging by that description, a cursory look at the code, and the observed behaviour, I assume git-absorb works something like the following:

  • Break each file into "hunks" of changed lines
  • For each hunk create a "virtual" commit
  • Progressively "move" the virtual commit up the stack of commits, until you find a commit that the virtual commit doesn't commute with.
    • In other words, find the first commit that "touches" the same hunk in the file. Attempting to move the virtual commit earlier would give a merge-conflict.
    • If the virtual commit doesn't conflict with any of the previous commits, git-absorb logs a warning, and leaves the hunk in the staging area, without committing it.
  • Once the target commit is found, generate the fixup! commit. Repeat for all hunks in all staged files.

The following shows a small example of this, in which a file has been changed in 4 commits. The staging area contains a bug fix which adds = -1 to the field initializer. git-absorb works out that the changes in commit B don't commute with the staged changes, so it generates a commit as fixup! B. This can then be squashed into the B commit.

Image of Rider showing the auto-squashed commits

Theoretically, git-absorb should work well as long as you have self contained commits. By definition, it also won't produce merge conflicts (which is certainly possible when manually creating your fixup-conflicts).

That's a limitation, in a way, as sometimes my large rebases necessarily introduce conflicts in order to create a more logical overall order, but it's not feasible to expect an automated tool to understand how or when to do that. And I don't think I'd want it to!

Now I've covered how git-absorb works and why you might want to try it, I'll describe how to install it.

Installing and configuring git-absorb

The first step is to download the latest binaries from the GitHub releases page at: https://github.com/tummychow/git-absorb/releases. If you're on Linux, it's also available in many package managers so you can apt install git-absorb, for example.

I'm on Windows, so I downloaded the latest windows x64 binary from the releases page:

There appears to be two Windows binaries, one tagged as msvc and one tagged as gnu. As far as I can tell, it doesn't make any difference which you use, but I used the MSVC one.

Even the Windows files are tar.gz files, so you'll need to un-tar them. Luckily PowerShell has a built in tar command (or you could alternatively hop over to WSL). I un-tar-ed the file using the magic -xvzf incantation:

tar -xvzf .\git-absorb-0.6.10-x86_64-pc-windows-msvc.tar.gz

which unpacks the files to a directory:

x git-absorb-0.6.10-x86_64-pc-windows-msvc/
x git-absorb-0.6.10-x86_64-pc-windows-msvc/doc/
x git-absorb-0.6.10-x86_64-pc-windows-msvc/doc/git-absorb.1
x git-absorb-0.6.10-x86_64-pc-windows-msvc/doc/git-absorb.txt
x git-absorb-0.6.10-x86_64-pc-windows-msvc/git-absorb.exe
x git-absorb-0.6.10-x86_64-pc-windows-msvc/LICENSE.md
x git-absorb-0.6.10-x86_64-pc-windows-msvc/README.md

As this was all in my Downloads folder, I moved the files to somewhere sensible:

mv .\git-absorb-0.6.10-x86_64-pc-windows-msvc\ c:\tools\git-absorb\

All that remains is to "register" the git-absorb binary (git-absorb.exe on Windows) with git. There's two ways we could do that:

  • Ensure the git-absorb.exe file is in your PATH
  • Add an alias to git to invoke the binary

I chose to do the latter and to add the alias. To add an alias called absorb, run the following command:

git config --global alias.absorb '!c:/tools/git-absorb/git-absorb.exe'

This ensures that git absorb invokes the git-absorb.exe binary, passing along any additional arguments.

Note that the path separators must be Unix / separators rather than Windows \ separators.

git-absorb is now installed and configured, so you can take it for a spin! In the next section I look at some of the warnings I ran into while playing with it, and some of the configuration options.

Exploring all the git-absorb options

Before we look at some of the available options, I'll cover a few of the error/warning messages you may see when running git absorb:

WARN No additions staged, try adding something to the index.

This one is pretty self-explanatory. git-absorb works on the git staging area/index, so you need to add your files using git add <File> as you would when you normally commit. This also means you can run git-absorb on a limited set of files at a time

WARN Please use --base to specify a base commit.
CRIT No commits available to fix up, exiting

You may see this error if you run git absorb but the commit you are expecting to fixup was a lot of commits earlier in your branch. By default, git-absorb only looks back 10 commits before giving up. You can increase this number for changing the maxStack variable, for example by running:

git config --global absorb.maxStack 50 

This will make git absorb consider a maximum of 50 commits. Obviously that may mean it takes git-absorb longer when it "fails" to find an appropriate non-commutable commit.

Alternatively, you can provide the "base" commit using --base when you call git absorb. For example:

git absorb --base origin/main

This will check all commits between HEAD and the commit supplied as --base (origin/main in this case).

You can see the other options available if you run git absorb -h:

git-absorb 0.6.10
Stephen Jung <[email protected]>
Automatically absorb staged changes into your current branch

USAGE:
    git-absorb.exe [FLAGS] [OPTIONS]

FLAGS:
    -r, --and-rebase    Run rebase if successful
    -n, --dry-run       Don't make any actual changes
    -f, --force         Skip safety checks
    -h, --help          Prints help information
    -V, --version       Prints version information
    -v, --verbose       Display more output
    -w, --whole-file    Match the change against the complete file

OPTIONS:
    -b, --base <base>                          Use this commit as the base of the absorb stack
        --gen-completions <gen-completions>    Generate completions [possible values: bash fish, zsh, powershell, elvish]

Of particular note in this list:

  • --and-rebase. Automatically runs git rebase -i --autosquash after generating the fixup! commits. This really makes the command look like magic, as your changes are seamlessly absorbed into the parent commits!
  • --dry-run. If you're not ready to trust git absorb yet, you can have git absorb just list what it would have done.

If you use the --dry-run option, you'll get output something like the following:

INFO would have committed, header: -1,1 +1,1, fixup: Add base types
INFO would have committed, header: -1,1 +1,1, fixup: Add base types
INFO would have committed, header: -0,0 +1,1, fixup: Add implementation

Which tells you how many fixup commits would have been generated, but it doesn't really help understand what's going into those commits, so I don't see it being very useful. By default git-absorb isn't destructive—it only adds commits—so I think it's easier to just run git absorb, and then explore the commits it made. If you don't like the results, you can use git reset HEAD~ to "remove" the last commit (or git reset HEAD~3 to remove the last 3, for example).

All in all, I'm impressed with the potential of git absorb. I've only just started using it for some toy projects, but I'm going to put it through its paces soon. If, like me, you weren't aware that it existed, give it a try and see what you think! And if you have any tips for me, leave them in the comments 🙂

Summary

git-absorb is a "plugin" for Git that can automatically create fixup! commits for use with git rebase --autosquash. It analyses the changes to your files and your commit history to figure out how to generate fixup! commits so that running git rebase -i --autosquash automatically includes the commits at the correct place. This can be extremely useful for addressing feedback/fixing bugs while still keeping your commit history clean.

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