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!
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
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:
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.
--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 😩
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:
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"!
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! 🎉
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 absorbworks 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-absorblogs 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
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
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
- 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
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
Before we look at some of the available options, I'll cover a few of the error/warning messages you may see when running
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
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 --autosquashafter 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 absorbyet, you can have
git absorbjust 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 🙂
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.