Ever wanted to git amend but for a commit that isn’t HEAD? You’re not gonna believe this…

function gspc -a COMMITS_AGO
  set -lx GIT_SEQUENCE_EDITOR "sed -i '' '1s/^pick/edit/; /TEMP_COMMIT_FOR_STAGED_CHANGES/s/^pick/drop/'"
	git stash push -k
	git commit -m TEMP_COMMIT_FOR_STAGED_CHANGES
	set -lx TEMP_COMMIT_HASH (git rev-parse HEAD)

	set -l REBASE_POSITION (math "$COMMITS_AGO + 1")
	git rebase -i HEAD~$REBASE_POSITION
	git cherry-pick -n $TEMP_COMMIT_HASH
	git commit --amend --no-edit
	git rebase --continue

	set -e GIT_SEQUENCE_EDITOR
	set -e TEMP_COMMIT_HASH
	git stash pop
end

This function is written in fish but you can easily replicate it in the shell of your choice. What does gspc stand for? Git [something] to previous commit (I forgot). First let’s demonstrate usage, we will add the changes of somefile to whatever commit is before the commit that’s before our latest commit:

> git add somefile
> gspc 3

Other commits and unstaged changes will be unaffected, and the COMMITS_AGO argument can be any number, just be careful not to insert changes in your history that would cause conflicts down the line, as it’s a pain to resolve. Let’s look at how it works:

1. Define a sed command that will be run automatically during interactive rebase

set -lx GIT_SEQUENCE_EDITOR "sed -i '' '1s/^pick/edit/; /TEMP_COMMIT_FOR_STAGED_CHANGES/s/^pick/drop/'"

We will tell git to edit the first (oldest commit) in the next interactive rebase that we initiate. We replace pick with edit to do so. We are also telling it to drop the commit named TEMP_COMMIT_FOR_STAGED_CHANGES

2. Stash unstaged changes, commit staged changes

Our commit here is temporary. We store the commit hash for later.

git stash push -k
git commit -m TEMP_COMMIT_FOR_STAGED_CHANGES
set -lx TEMP_COMMIT_HASH (git rev-parse HEAD)

3. Initiate git rebase

To edit the commit that is 3 commits ago, we need to execute a rebase that looks like HEAD~4, so we add 1 before initiating our interactive rebase.

set -l REBASE_POSITION (math "$COMMITS_AGO + 1")
git rebase -i HEAD~$REBASE_POSITION

Since we defined and exported the GIT_SEQUENCE_EDITOR variable previously, git will not open our editor for us to manually edit, even though it’s an interactive rebase, it will run our sed command instead. Since our sed command replaced the first pick with edit, however, the rebase will still be in progress at that commit, waiting for us to make changes (edit) and resume the rebase.

4. Cherry pick our staged changes and continue

-n during cherry-pick prevents a new commit from being created, we instead amend the previous commit with our changes, then continue the rebase. It doesn’t matter than TEMP_COMMIT_HASH is no longer part of git history, the commit can be anywhere as long as it’s not pruned.

git cherry-pick -n $TEMP_COMMIT_HASH
git commit --amend --no-edit
git rebase --continue

When the rebase continues, git will continue executing our instructions, one of which is to drop TEMP_COMMIT_FOR_STAGED_CHANGES so that we are fully cleaning up after ourselves.

5. Clean up, restore unstaged changes

Here we erase the variables we created, to prevent git rebase from re-using the same instructions when we do a different rebase later.

set -e GIT_SEQUENCE_EDITOR
set -e TEMP_COMMIT_HASH
git stash pop

Popping our stash brings back the unstaged changes that we didn’t want to insert into our previous commit.

Surely there’s a GUI way to do it?

Sublime Merge can do it, but requires you to have the changes committed. You can squash any number of commits together into the oldest commit by selecting multiple commits and right clicking: