Skip to content

Instantly share code, notes, and snippets.

@fcamblor
Forked from eddiemoya/git-flip-last.sh
Last active August 5, 2022 16:00
Show Gist options
  • Save fcamblor/3202f6ca617feac902c4c267a48e0d8b to your computer and use it in GitHub Desktop.
Save fcamblor/3202f6ca617feac902c4c267a48e0d8b to your computer and use it in GitHub Desktop.
Flip the last two commits in a branch using git-cherry-pick, git-update-ref and git-rev-parse. Interesting exercise using quite a bit of plumbing commands.
#!/bin/bash
flip_last_branch_name=ongoing-flip-last;
git rev-parse --verify $flip_last_branch_name > /dev/null 2>&1;
res=$?;
if [ "$res" = "0" ]; then
echo "An ongoing flip-last is already in progress ($res) ... execute following command to remove it : git branch -D $flip_last_branch_name";
exit -1;
fi;
pendingChangesInIndex=$(git status -s --untracked-files=no | grep "^[^?]" | wc -l | tr -d '[[:space:]]')
if [ "$pendingChangesInIndex" != "0" ]; then
git stash
fi
current_branch_name=$(git rev-parse --abbrev-ref HEAD);
git checkout -b $flip_last_branch_name;
git reset --keep HEAD^^;
git cherry-pick $current_branch_name $current_branch_name^1;
res=$?;
if [ "$res" -ne "0" ]; then
stashCommand=""
if [ "$pendingChangesInIndex" != "0" ]; then
stashCommand="git stash pop; &&";
fi
echo "Error during cherry pick detected ... please fix the issues then execute following command until fully resolved :";
echo "git cherry-pick --continue && git update-ref refs/heads/$current_branch_name $(git rev-parse HEAD) && git checkout --quiet $current_branch_name && git branch -D $flip_last_branch_name && $stashCommand echo 'Commits swapped successfully !'";
exit -1;
fi;
git update-ref refs/heads/$current_branch_name $(git rev-parse HEAD);
git checkout --quiet $current_branch_name;
git branch -D $flip_last_branch_name;
if [ "$pendingChangesInIndex" != "0" ]; then
git stash pop
fi
echo 'Commits swapped successfully !';
# Instead of creating an independant bash script with the code above,
# consider simply creating a git alias using the command below.
#
# USAGE: git flip-last
git config --global alias.flip-last '!flip_last_branch_name=ongoing-flip-last;git rev-parse --verify $flip_last_branch_name>/dev/null 2>&1;res=$?;if [ "$res" = "0" ];then echo "An ongoing flip-last is already in progress ($res) ... execute following command to remove it : git branch -D $flip_last_branch_name";exit -1;fi;pendingChangesInIndex=$(git status -s --untracked-files=no|grep "^[^?]"|wc -l|tr -d '"'"'[[:space:]]'"'"');if [ "$pendingChangesInIndex" != "0" ];then git stash;fi;current_branch_name=$(git rev-parse --abbrev-ref HEAD);git checkout -b $flip_last_branch_name;git reset --keep HEAD^^;git cherry-pick $current_branch_name $current_branch_name^1;res=$?;if [ "$res" -ne "0" ];then stashCommand="";if [ "$pendingChangesInIndex" != "0" ];then stashCommand="git stash pop; &&";fi;echo "Error during cherry pick detected ... please fix the issues then execute following command until fully resolved :";echo "git cherry-pick --continue && git update-ref refs/heads/$current_branch_name $(git rev-parse HEAD) && git checkout --quiet $current_branch_name && git branch -D $flip_last_branch_name && $stashCommand echo '"'"'Commits swapped successfully !'"'"'";exit -1;fi;git update-ref refs/heads/$current_branch_name $(git rev-parse HEAD);git checkout --quiet $current_branch_name;git branch -D $flip_last_branch_name;if [ "$pendingChangesInIndex" != "0" ];then git stash pop;fi;echo '"'"'Commits swapped successfully !'"'"';'

I typically use this command during big refactoring when I have plenty of modified files in my working copy and I start a "small subtask" and don't want to spend time using rebase -i to isolate my small changes amongst my whole refactoring.

So I :

Why not simply use stash + commit + stash pop ?

Mainly for 2 reasons :

  • Sometimes, we want to flip 2 commits "after" the commit has been performed .. so the commit cannot be "introduced" between stash / stash pop
  • Sometimes, we want to test our small commit with our work in progress. Using stash will not allow this.
    Example : renaming a function used at 10 location calls. Let's consider 1 location call has been added during our "work in progress".
    If I use git stash, then my function rename will be applied at only 9 locations. I will have to ensure that my 10th location is taken into consideration when I will git stash pop (on dynamic language, I can miss it)
    If I use git flip-last, I will perform the rename after my wip commit, so it will be impacted on the 10th location. I will have a conflict during flip-last (because the 10th location will not exist in the codebase) but this won't be a complicated conflict to solve at that time.

Why not simply use commit + rebase -i HEAD^^ ?

Yes I could git wip+ git commit + git rebase -i HEAD^^ + swap latest 2 commit in editor and validate + git unwip

My concern here is I don't want to spend 4-5s to open editor and swap 2 first lines : this will fluidize my workflow and will encourage me to make those small commits more frequently

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment