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: