Programmatically swap last two commits
I know how to swap the last two commits using
git rebase interactively (
git rebase -i HEAD~2,
ddjp:x in Vim), but I’d like to do it programmatically with a wrapper script since it’s something I end up doing relatively often.
To be more concrete, I want to rewrite history from
in an entirely scripted manner. Ideally, if the swap fails, it should either allow me to fix it interactively or just give up and tell me to do it manually.
3 Solutions collect form web for “Programmatically swap last two commits”
This should do it:
git tag old git reset --hard HEAD~2 git cherry-pick old git cherry-pick old~1 git tag -d old
First, you tag the place where you are as
old, then go back two commits,
git cherry-pick the commits in the other order, and delete the
Since you want to do this often, I assume your going to want to reduce to a single step process. I’ll make this a bit educational though and break it down.
Don’t do this sort of thing on any commits that have already been shared with other developer, or pushed to a remote. Rewriting shared history is a recipe for disaster.
That public service announcement aside…
A---B---C---D (master, HEAD, ORIG_HEAD) git rebase --quiet --onto HEAD~2 HEAD~1 HEAD
This takes whatever is in HEAD that isn’t in HEAD~1, and applies it to HEAD~2. After running this rebase, you will have this history. Keep in mind that
git-rebase --onto will throw you into a headless state. (I’ve got
--quiet in there so the final command doesn’t spit a wall of text to your screen).
A---B---C---D (master, ORIG_HEAD) \ D' (HEAD)
Now we need to get
C applied after
D', for this we can use
git-cherry-pick. When a
git-rebase --onto is performed as it was above, the original commit history before the rebase is saved in
ORIG_HEAD because git wont change it until you do some other activity. This is useful in case you screw up a rebase, but we’ll use it here to cherry-pick.
git cherry-pick ORIG_HEAD~1 A---B---C---D (master, ORIG_HEAD) \ D'---C' (HEAD)
The state of HEAD is now exactly as you wanted, with just two commands. I assume this will usually take place from within a branch, and that you’ll want to update that branch with the new order of commits. If I’m wrong about that, then that’s it and your done. If you do want update the branch you were on, there are a couple ways to do that.
The simplest to do manually would be to do the following…
git log -1 ###copy the the SHA1 git checkout master git reset --hard <SHA1>
However the whole point is automation, and there ways of doing this that are less obvious, but require fewer commands.
git update-ref refs/heads/master $(git rev-parse HEAD)
git-rev-parse, I get only the commit of HEAD and nothing else. Applying that to `git-update-ref, I can “reset” the master branch without having to check it out first. Now that master is set properly, I can go ahead and checkout master (the point of the update-ref is to reduce the number of steps involved for the eventual alias/bash script).
git checkout --quiet master
Again, I pass
--quiet so as to reduce the amount of text dumped on the screen after each command.
If you wanted to do this as a bash script, you could automate the entire process even further, and make it work dynamically on whatever branch you want, not just master.
#!/bin/bash branch=$(git name-rev --name-only HEAD) git rebase --onto HEAD~2 HEAD~1 HEAD git cherry-pick ORIG_HEAD~1 git update-ref refs/heads/$branch $(git rev-parse HEAD) git checkout --quiet $branch
That said, this is just as easily dumped into a git alias without having to actually create a file for the bash script. Creating the file and setting up the alias isn’t hard by any means, but it’s another thing to understand, and many people don’t. Here’s a simple git alias, run this command once..
git config --global alias.flip-last "!branch=$(git name-rev --name-only HEAD); git rebase --quiet --onto HEAD~2 HEAD~1 HEAD; git cherry-pick ORIG_HEAD~1; git update-ref refs/heads/$branch $(git rev-parse HEAD); git checkout --quiet $branch"
Now anytime you want to flip your last two commits, just use…
This was an interesting little script to write, I’ve thrown it up on a github gist. Feel free to fork it, make changes, star it, whatever.
For reference, this is the script I ended up using:
. "$(git --exec-path)/git-sh-setup" require_clean_work_tree swap2 if [ -e $GIT_DIR/sequencer ]; then die "Cherry-pick already in progress; swap aborted" fi HEAD=`git rev-list --max-count=1 HEAD` git reset --hard HEAD~2 if git cherry-pick $HEAD $HEAD^; then echo "Successfully swapped top two commits." else git cherry-pick --abort git reset --hard $HEAD die "Failed to swap top two commits." fi