March 2022 edit: check out git-revise, which can do this and more, in a much more natural way.
Everyone knows and loves to use git commit --amend
to change the latest commit. But what if you want to correct a older commit?
The flow in that case involves an interactive rebase with a edit step. But that's kludgy. Here's an alias that using a couple of nifty git features makes it one command analogous to commit --amend
.
[alias]
fixup = "!f() { TARGET=$(git rev-parse "$1"); git commit --fixup=$TARGET ${@:2} && EDITOR=true git rebase -i --autostash --autosquash $TARGET^; }; f"
Install it by adding it to ~/.gitconfig
.
Then use git fixup COMMIT
to change a specific "COMMIT", exactly like you would use git commit --amend
to change the latest one.
You can use all git commit
arguments, like -a
, -p
and filenames. It will respect your index, so you can use git add
. It won't touch the changes you are not committing.
For example, to add the changes you made to the Makefile (and only those) to the second to last commit, you'd run:
$ git fixup HEAD^ Makefile
[master 3fff270] fixup! Hello, world!
1 file changed, 10 insertions(+), 0 deletions(-)
create mode 100644 Makefile
[detached HEAD 79c91d1] Hello, world!
Date: Thu Jun 30 11:00:52 2016 -0700
13 files changed, 235 insertions(+), 159 deletions(-)
create mode 100644 Makefile
delete mode 100644 main.go
Successfully rebased and updated refs/heads/master.
How it works
Let's break it down a little. It works in two steps.
First we make a git commit
with --fixup
. --fixup=TARGET
sets the new commit's message to fixup!
plus the target commit message. The first git fixup
parameter is used as the target and all others are passed as-is, so it behaves exactly like git commit
.
Then we make an interactive rebase (git rebase -i
) with --autosquash
. --autosquash
takes all commits with a message like fixup! FOO
, reorders them to follow the commit named FOO
and marks them as fixups (like squash, but ignoring the message). It's made, as you guessed, to work with git commit --fixup
.
--autostash
ensures the not-committed changes are stashed before the rebase and popped after it's done. The rebase base is the commit coming immediately before the target one, $TARGET^
. EDITOR=true
uses the true
binary (which exits 0 immediately) as the editor, so you don't have to see and save the interactive listing.
We save the git rev-parse
result as a TARGET variable so that if you used something relative like HEAD^
the reference doesn't change when we make the commit.
Finally, we wrap it all in a function so that we have access to the arguments as $1
and ${@:2}
, and we set it as a shell alias (instead of a git command one) by prepending !
.
For more convoluted git magic, follow me on Twitter.