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.