A clean commit history with Git WipSquash

| 4 min read

Maintaining clean and meaningful commit histories in Git is crucial for collaboration and code maintenance.
Yet, the process of ensuring every commit is atomic can feel tedious, especially during active development.
I like to do super small commits, especially when I’m refactoring code or working on something I’m not super sure about, but I’m way too lazy to type a meaningful message each time.

Maybe someday I’ll write a Jetbrains IDE extension that generates commit messages after an automated refactoring, but as that doesn’t exist yet, I sometimes go with a commit containing only wip (for Work In Progress).
It takes no time to create, you can even have an alias for it, but it left you with a commit history that you are not proud of, and clearly not meaningful at all.

I wanted to find a solution and finally took some time to work on it and create a script that solves my pain point.
I’m currently experimenting with it and with that workflow, and I wanted to share it with others to see if some people find that idea useful.

Enters git wipsquash

Let’s talk about git wipsquash.

The idea for wipsquash is that you make an effort to say what you are doing only once, at the start.
This goes well with atomic/conventional commits.

Imagine you’re working on a feature or bug fix. You start with an intentional, well-named commit, such as:

git commit -m “Add authentication middleware”

Then, as you continue tweaking, experimenting, or debugging, you add several more commits, hastily labeled wip:

git commit -m “wip”
git commit -m “wip”

Now you’re done. This is a great, good job, but the commit history looks like this:

1. Add authentication middleware
3. wip
2. wip

Will you be brave enough to push that to the shared repo with your colleagues?
What do you feel about that commit history?
I don’t know about you, but I know that I feel a bit ashamed.

This is the time to call wipsquash.

wipsquash allows you to combine your wip commits with the nearest meaningful one, creating clean, atomic commits without disrupting your workflow.

Calling git wipsquash merges the three commits together and keeps them at one, properly named Add authentication middleware.

When git wipsquash is called without any parameter, it will squash up to the first past commit with a meaningful name (defined here a not containing wip or WIP or any variation of that).
When called with a commit hash, the script will run will squash up to that commit, creating as many commits as you have non-wip commits.
For instance, with that commit history

0. ...
1. Add authentication middleware
2. Wip
3. wip
4. Add error message when people are not logged in
5. wIp

git wipsquash 1

will produce

0. ...
1. Add authentication middleware
2. Add error message when people are not logged in

with the right commits squashed in the right place.

A little aside on keeping a history of all the small changes

Sure, we are losing all the small steps that we took during the implementation.
Some teams like to keep them, others don’t.
My opinion about that changed a few times.
At the moment, it is that I like having the small commits while I’m working on the feature (and this is why I would like to automate the naming and not just go with wip), but I’m not sure of the value of keeping them after that.

Getting wipsquash

Copy the script below into a file named git-wipsquash in a directory in your PATH, and give it execution right with chmod +x git-wipsquash.

As you can see, the scripts actually runs a git rebase -i’ and makes a lot offixup` commits.

#!/bin/sh

# git-wipsquash: Rewrite the commit history on the current branch so that
# any commit containing "wip" in its message is squashed into the nearest
# preceding commit that does NOT contain "wip".

START_COMMIT="$1"

# If no start commit is given, find the most recent commit that doesn’t contain "wip" in the message.
if [ -z "$START_COMMIT" ]; then
# Walk from HEAD backwards, find the first (i.e., newest) commit without "wip".
for C in $(git rev-list HEAD); do
SUBJECT=$(git log -1 --pretty=%s "$C")
if ! echo "$SUBJECT" | grep -iq "wip"; then
START_COMMIT="$C^"
break
fi
done

if [ -z "$START_COMMIT" ]; then
echo "No commit found without 'wip' in its message. Aborting."
exit 1
fi
fi

echo $START_COMMIT

# 1) Make sure START_COMMIT is an ancestor of HEAD.
if ! git merge-base --is-ancestor "$START_COMMIT" HEAD; then
echo "ERROR: $START_COMMIT is not an ancestor of HEAD. Aborting."
exit 1
fi

# 2) Collect commits (oldest first) strictly after START_COMMIT up to HEAD.
COMMITS=$(git rev-list --reverse "${START_COMMIT}..HEAD")


# 2) Build the interactive rebase instruction list
INSTRUCTIONS=""
for C in $COMMITS; do
SUBJECT=$(git log -1 --pretty=%s "$C")
if echo "$SUBJECT" | grep -iq "wip"; then
# squash into the previous picked commit using fixup to avoid prompting a message
INSTRUCTIONS="$INSTRUCTIONS
f $C"

else
# pick normal commits
INSTRUCTIONS="$INSTRUCTIONS
p $C"

fi
done


# 3) Write the instructions to a temp file
TMPFILE=$(mktemp)
# The leading newline in $INSTRUCTIONS can confuse 'git rebase -i', so trim it:
echo "$INSTRUCTIONS" | sed '/^[[:space:]]*$/d' > "$TMPFILE"


# 4) Run interactive rebase:
# Rebase the listed commits on top of START_COMMIT
# so everything after START_COMMIT is subject to rewriting.
GIT_SEQUENCE_EDITOR="cat $TMPFILE >" git rebase -i "$START_COMMIT"


# 5) Clean up
rm -f "$TMPFILE"

You’re ready to go.

Also, you might find it interesting to create an alias for quickly creating git wip commits.
Add the following alias to your .gitconfig:

[alias]
cwip = "! git add . && git commit -m ‘wip’ --no-verify”

You can now type git cwip to add all your changes and create a wip commit.
Because this adds all your changes you want to run it often, to know what you are commit without the need to review it.

Conclusion

git wipsquash seems to solve my issue with super small commits and keep a global shared commit history clean. It’s a simple yet powerful tool to automate tedious commit cleanup, that would otherwise take too long.

Whenever you're ready, here is how I can help you: