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

  • composer private package based on gitlab is identified as git submodule
  • Gradle: Passing variable from one task to another
  • How to push commits from changes made to a submodule into main git repo?
  • Setting up Libgdx project with git
  • Efficiently updating a git submodule from a sparse checkout of the main repository
  • Git: How to correct a bad merge after more commits have happened?
  • A---B---C---D HEAD
    

    to

    A---B---D---C HEAD
    

    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.

  • See what git fetch displayed last time
  • 'git add .' fails when local repository is in subdirectory of the worktree
  • git commit in pre-push hook
  • How to git diff multiple sibling git folders
  • Synchronize vim settings across mac and unbuntu server when using NeoBundle
  • Git - How to revert origin remote remove
  • 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 old tag.

    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.

    IMPORTANT

    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…

    Step 1

    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.

    Step 2

    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.

    Step 3

    git update-ref refs/heads/master $(git rev-parse HEAD)
    

    By using 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).

    Step 4

    git checkout --quiet master
    

    Again, I pass --quiet so as to reduce the amount of text dumped on the screen after each command.

    Bash Script

    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
    

    Git Alias

    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…

    git flip-last
    

    GitHub Gist

    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.

    https://gist.github.com/eddiemoya/5456992

    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
    
    Git Baby is a git and github fan, let's start git clone.