One of the things I like about Git is the ability to fixup commits. It’s a great way to keep the history clean and make sure that changes are grouped together, in logical, atomic commits.
This makes reviewing easier, and I want to provide tooling to other folks that might not know about this feature at all – or might not be using it as much as they could because it’s a bit cumbersome.
Intro
The problem with git commit --fixup
is that it requires you to provide the commit hash of the commit you want to fixup. The usual workflow is to blame the line you want to fix (which is a pain, because you’ll have a ton of Not Committed Yet
lines), copy the hash, and then run git commit --fixup <hash>
.
Helpful tip though, you can get rid of the Not Committed Yet
lines by adding HEAD
to the blame command. For example, git blame HEAD -- <file>
, but that’s not something that the Sublime Text plugin I use does.
Then, when you’re ready to squash the fixup commit, you need to run git rebase -i --autosquash main
, it opens the editor, then save and quit.
Hopefully, you correctly identified the commit you wanted to fixup, and you’re done.
Now, imagine we get a PR review, and we have 10s of changes to make. That’s a lot of manual work.
Automated fixup with gfra
Now the function I’ll introduce doesn’t solve the problem of identifying the commit you want to fixup, but it does automate the process of creating the fixup commit and rebasing interactively to autosquash the changes.
gfra() {
git commit --fixup="$1" --no-verify; git rebase --autosquash "$1^"
}
Bash experts out there will say “ha, that’s simple”, and it is. Easier than going back again and again in your history to find the command though.
The gfra
name is inspired from the zsh git aliases family, and are just initials for git fixup rebase autosquash
, just like gs
is git status
.
Usage
The usage is simple:
gfra <commit-hash>
Automating Commit Fixes with gfrauto
While gfra
nails the process of fixing and rebasing a single commit, gfrauto
takes it a step further – or more like ten – by automating the detection of the commit that modified a particular line in a staged file. No more manual blame, copy-pasting, or rebase editing.
Without further ado, here is the function (zsh syntax for the read
commands, so be careful if you use it in bash):
gfrauto() {
# Fist, display the changes
git diff --staged
read "?Continue? ^C to cancel"
# Extract the line number from the diff
line=$(git diff --staged --unified=0 | grep -Po '^@@ -[0-9]+(,[0-9]+)? \+\K[0-9]+(,[0-9]+)?(?= @@)');
# If we have multiple files, arbitrarily pick the first one
line=$(echo $line | head -1);
# Line is either a single line or a range of lines
# E.g. 10 or 10,20
# If it's a range, we need to split it and get the first line
line=$(echo $line | cut -d, -f1);
# Extract the file name from the diff
file=$(git diff --staged --unified=0 | grep -Po '^\+\+\+ ./\K.*');
# If we have multiple files, arbitrarily pick the first one
file=$(echo $file | head -1);
# Find the commit that last modified the line in the file
commit=$(git log -u -L $line,$line:$file -1 --pretty=format:"%h" | head -1);
# Display the hash, the description of the commit for confirmation
git show --no-patch --format="%h %s" $commit
read "?Continue? ^C to cancel"
# Let the magic happen
gfra $commit
}
Usage
- Stage your changes, as usual, keep them light since the script is dumb and will just base its search on the first file it finds. Make sure you only stage changes that belong to the same commit.
- Run
gfrauto
and confirm the diff, then the found commit. - That’s it, it’s fixed up and rebase automatically
The best part is that if your shell history dedupes entries, you won’t ever clutter your history with different calls to the gfra
function.
Conclusion
I’ve used gfra
for a while and consider it battle tested. The first usage in my zsh history is on dates from more than 5 years ago and I use it multiple times a day. I however don’t pretend to take credit on such a simple function.
For gfrauto
, it’s been present as a long one-liner in my history, refined over time. Here is it for posterity:
# Do not use this
gfra $(line=$(gd --staged --unified=0 | grep -Po '^@@ -[0-9]+(,[0-9]+)? \+\K[0-9]+(,[0-9]+)?(?= @@)'); file=$(gd --staged --unified=0 | grep -Po '^\+\+\+ ./\K.*'); git log -u -L $line,$line:$file -1 --pretty=format:"%h" | head -1)
Obviously, it’s more cryptic, handled less edge cases, so I finally took the time to write it down in a more readable form, and I’m happy to share it today.