We all know how to use git commit --amend
to fix up the very last commit.
How beautiful would it be to have a command like:
git commit --amend=3
to extend this power to fix up the 3rd to last — or any other past — commit?
Enter git fixup.
Add this alias to your .gitconfig
:
[alias]
fixup = "!f() { TARGET=$(git rev-parse $1); git commit --fixup=$TARGET ${@:2} && GIT_SEQUENCE_EDITOR=true git rebase -i --autostash --autosquash $TARGET^; }; f"
Then, use it as follows:
git add .
git fixup <SHA-1>
).Some devs are scared by the very notion of rebasing, but in fact a rebase is an embarassingly simple thing: just a series of cherry-picks.
With a tree such as:
* 84831ab (HEAD -> branch) branch 3
* 65b297f branch 2
* 66fe73c branch 1
| * 7208745 (master) five
| * 6f3ab93 four
|/
* 85f4b7e three
* 7011e1f two
* 76ec393 one
rebasing branch
on top of master
with:
git rebase master
is just equivalent to:
git reset --hard master
git cherry-pick 66fe73c # 1st commit
git cherry-pick 65b297f # 2nd commit
git cherry-pick 84831ab # 3rd commit
Adding the --interactive
flag to git rebase
lets you fine tune the
series of cherry-picks before their execution.
If instead of the previous:
git rebase master
you run:
git rebase master --interactive
your preferred editor will pop-up, with this content:
pick 66fe73c 1st commit
pick 65b297f 2nd commit
pick 84831ab 3rd commit
This gives you the chance to do all sorts of magic before rebasing, such as
and the like.
In other words, --interactive
creates a transient todo-list file, a
little script of commands for Git to execute.
This is the first key to understanding how git fixup
works.
The second key revolves around a little known option of the git
rebase
command: --autosquash
.
As a matter of fact, not only can the user edit the todo-list file,
but Git itself also interprets the commit messages in search of
commands. This is similar to the functionality often used with GitHub
to close an issue by mentioning its number with a special syntax such as
#fixes 42
in a commit message.
Indeed, during an interactive rebase:
If a commit message starts with “squash! “, “fixup! “ or “amend! “, the remainder of the subject line is taken as a commit specifier, which matches a previous commit if it matches the subject line or the hash of that commit.
In order to instruct Git to interpret those commands, you shall use
the (confusingly named) --autosquash
option.
So, the trick for fixing up the 3rd to last commit is to add a new commit with a properly forged message mentioning its message:
pick 3732234 fixup! 3rd commit
pick 66fe73c 1st commit
pick 65b297f 2nd commit
pick 84831ab 3rd commit
and then running an interactive rebase with the --autosquash
option.
We are almost there. Let’s fine tune this idea. As a matter of fact, you don’t even have to type that message. Just type:
git commit --fixup 84831ab
and Git will diligently copy the 3rd to last commit message, prefixing
it with fixup!
.
Now it’s a matter of executing the interactive rebase, with this magic
--autosquash
option.
The only outstanding problem is that for this fixup to work, the
rebase must be interactive and this is not what you wished: you don’t
really want to be asked for any input. You prefer having a git fixup
command completing all the necessary tasks without bothering you.
The trick is to use true
as the interactive editor.
true
is a little tiny program that does nothing, successfully (it
takes 80 lines to do so).
This should give you all the ingredients to interpret the alias.
If you are one of those horrible developers who refuse to copy-paste a
snippet of code before they really understand it, here’s a breakdown.
Part | Meaning |
---|---|
!f() { ... }; f |
Define a shell function named f ; at the end, execute it. |
TARGET=$(git rev-parse $1); |
Get the full commit hash of the target commit, specified in the first Bash argument ($1 ). |
git commit --fixup=$TARGET |
Create a new commit setting the commit message to fixup! <commit message of the target commit> . |
${@:2} |
Optionally, allows you to pass additional arguments to commit. ${@:2} in Bash refers to all the command line arguments starting from the second one. |
&& |
This is a shell operator that executes the next command only if the previous one succeeded. |
GIT_SEQUENCE_EDITOR=true |
Tell Git to use true as the dummy editor for the interactive rebase. true always succeedes, actually turning the interactive rebase as non-interactive |
git rebase |
Start the rebase… |
-i |
interactively… |
--autostash |
stashing any uncommitted changes before rebasing… |
--autosquash |
squashing the fixup commit into the target commit… |
$TARGET^; |
from the commit before (^ ) the target commit ($TARGET ). |
Magit, the amazing Git client running on top of
Emacs, offers a very convenient way for selecting the past
commit to modify. Just make your change. Then, instead of committing on
top of the last commit, run magit-commit-instant-fixup
and select
the commit you want to amend. That simple.
The feeling is like having a non-read-only Git history. So sweet!
If this is not enough, Magit offers a command called
magit-rebase-edit-commit
that lets you mark a past commit as
editable and perform whatever change you desire.
Yes, Magit is magic. But, of course, under the hood, there’s nothing but a simple, fixup rebase.
Enjoy rewriting history!