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.