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:
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:
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! 🎉
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.
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 <tummychow511@gmail.com>
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 runsgit rebase -i --autosquash
after generating thefixup!
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 trustgit absorb
yet, you can havegit 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.