Loading...

How To Squash All Commits Of Your Feature Branch

Productivity
August 8, 2019
5 minutes to read
Share this post:

Usually you have a lot of “intermediate” commits while developing on a feature branch like WIP, Review changes, Some cleanup, Fix jenkins. These commits are neither atomic nor does it help to read them in the history. They purely serve the purpose to persist the current work, trigger another build on your buildmachine or doing some fixes you discovered while testing. So before I rebase my changes to master I’d like to squash all commits that i’ve done to a single one. Usually I use interactive rebase for this purpose but since I need to actively tell every commit that I want to squash it I found it tedious. Also its easier, most of the time, to rebase onto master since you only apply conflicting changes to the end-result of your work without the intermediate steps you had to take. So I came up with my own git alias that solves this for me. Introducing: git squash <Commit/Branch> <Message>

TL;DR;

git config --global alias.squash '!bash -c '"'"'usage="Usage: git squash <Commit/Branch> <Message>";baseBranchParam=$0;if [ -z ${baseBranchParam+x} ];then echo "No base branch specified";echo $usage;exit 1;fi;git --no-pager show $baseBranchParam&>/dev/null;if [ $? -ne 0 ];then echo "No valid git object specified.";echo $usage;exit 1;fi;commitMsg=$1;if [ -z "$commitMsg" ];then echo "No commit message specified for squash commit.";echo $usage;exit 1;fi;currentBranch=$(git rev-parse --abbrev-ref HEAD);baseCommit=$(git merge-base $currentBranch $baseBranchParam);printf "Squashing the following commits:\n\n";printf "$(git --no-pager log --format='\"'%H %an - %s'\"' $baseCommit..$currentBranch)\n\n";git reset --soft $baseCommit&>/dev/null;git commit -m "$commitMsg" 1>/dev/null;if [ $? -ne 0 ];then echo "Squashed into new commit \"$commitMsg\"";exit 0;fi;exit 1'"'"' $1 $2'

Usage:

git squash <Commit/Branch> <Message>

Where <Commit/Branch> is the parent from where you started your branch and <Message> is the commit message for your squashed commit.

Long Version

Let’s first demonstrate what we want to achieve. Imagine we are in the following state in our development:

Usually I would simply rebase the feature branch onto master (rebase because in larger teams you want to avoid a merge hell ), but when we do so we end up with the following:

The two commits from our feature branch are not atomic so we now have “broken” commits on our master. This can make it harder to use tools like git bisect to figure out where bugs were introduced. Also this forces you to write more or less useful commit messages on your feature branch, even when you don’t have a working version and just want to persist your current state before leaving into the weekend. So I prefer to squash all my commits before I rebase the feature onto master.

Unfortunately it’s not possible to (easily) use git rebase --interactive and squash all commits. But thinking about what we want to achieve it’s actually easy with other commands. We want to get all changes until the commit we branched of, reset to it and commit all changes in a single commit. So basically the following is sufficient:

git reset --soft <base commit>
git commit -m <new commit msg>

As you might notice we also need to figure out the <base commit>. This can be done by using git merge-base <current branch> <base branch>. git merge-base will figure out the best common ancestor of two branches. But now we need to figure out the <current branch>. There are multiple ways to do that. In the most recent git version git branch --show-current does exactly what you expect, but since this is a rather new feature I decided to rely on an older approach which is git rev-parse --abbrev-ref HEAD. So we finally have our logic to do a squash with minimal input:

#!/bin/bash

baseBranchParam=$1
commitMsg=$2

currentBranch=$(git rev-parse --abbrev-ref HEAD)
baseCommit=$(git merge-base $currentBranch $baseBranchParam)

git reset --soft $baseCommit
git commit -m "$commitMsg"

Now we add a couple of safety checks and some nice output and we end up with a script like this:

#!/bin/bash

usage="Usage: git squash <Commit/Branch> <Message>"

baseBranchParam=$1

# Check if a base branch parameter is set
if [ -z ${baseBranchParam+x} ]; then 
	echo "No base branch specified"
	echo $usage
	exit 1
fi

# Check if the base branch parameter is an actual git object
git --no-pager show $baseBranchParam &> /dev/null

if [ $? -ne 0 ]; then 
	echo "No valid git object specified." 
	echo $usage
	exit 1 
fi 

commitMsg=$2

if [ -z "$commitMsg" ]; then
	echo "No commit message specified for squash commit."
	echo $usage
	exit 1
fi

currentBranch=$(git rev-parse --abbrev-ref HEAD)

baseCommit=$(git merge-base $currentBranch $baseBranchParam)

printf "Squashing the following commits:\n\n"
printf "$(git --no-pager log --format='%H %an - %s' $baseCommit..$currentBranch)\n\n"

git reset --soft $baseCommit &> /dev/null
git commit -m "$commitMsg" 1> /dev/null

if [ $? -ne 0 ]; then
	echo "Squashed into new commit \"$commitMsg\""
	exit 0
fi

exit 1

We are now able to use the script like ./git-squash master "Implement new feature" and it will squash all commits of the branch we are currently working on and put it into a new commit with the message “Implement new feature”.

The last piece missing now is that we also want to have it as a proper git alias. To do that we add the following entry into our .gitconfig:

[alias]
    squash = !bash -c 'bash /path/to/your/script/git-squash $1 $2'

Finally, we are able to use git squash master "Implement new feature".

Update 2019-08-14

I updated the script a bit to not redirect the error message when git commit fails. This is handy because you might have pre-commit hooks that cause it to fail and you want to see the actual error message.

Have you heard of Marcus' Backend Newsletter?

New ideas. Twice a week!
Top