Often in my work, I’ll create a number of commits before I craft my correct solution. Having these missteps in separate commits doesn’t provide any real value and adds noise so I’ll often squash these commits into a single commit. Historically, I’ve used a Git interactive rebase to do this but as I was reminded on Mastodon, it seems like there should be a quicker way, so I’ve created a Git alias, git squashbackto, to do this.
My Solution
This is the command I came up with to squash the last 5 commits.
git -c sequence.editor="vim --clean -c 'silent 2,\$s/^pick/squash/|:wq'" rebase --interactive --keep-base HEAD~5
Git Alias
git squashbackto
I’ve turned this solution into a Git alias, git squashbackto.
Running the following from the command line will add the git squashbackto alias to your Global Git Configuration file.
git config --global alias.squashbackto '!git -c sequence.editor="vim --clean -c '\''silent 2,\$s/^pick/squash/|:wq'\'' " rebase --interactive --keep-base'
The entry in your Global Git Configuration file will look like
[alias]
squashbackto = !git -c sequence.editor=\"vim --clean -c 'silent 2,\\$s/^pick/squash/|:wq' \" rebase --interactive --keep-base
How to Use
git squashbackto
We can squash the last five commits with
git squashbackto HEAD~5
or squash all of the commits added to our branch since we branched off main (i.e. all of the commits on our branch that don’t appear on main)
git squashbackto main
How I Tested This
In the process of testing this, I created an empty directory (I called mine squashcommits/) and cded into it. Once there I ran the following:
(rm -rf ./.git || true ) &&
(rm -f ./*.md || true ) && \
git init && \
echo '# My Project' > README.md && \
git add README.md && \
git commit -m 'Initial commit: add README.md' && \
git switch -c feat/myfeature && \
echo 'Feature (attempt 1)' > myfeature.md && \
git add myfeature.md && \
git commit -m 'Add my feature (attempt 1)' && \
echo 'Feature (attempt 2)' > myfeature.md && \
git add myfeature.md && \
git commit -m 'Add my feature (attempt 2)' && \
echo 'Feature (attempt 3)' > myfeature.md && \
git add myfeature.md && \
git commit -m 'Add my feature (attempt 3)' && \
echo 'Feature (attempt 4)' > myfeature.md && \
git add myfeature.md && \
git commit -m 'Add my feature (attempt 4)' && \
echo 'Feature (attempt 5)' > myfeature.md && \
git add myfeature.md && \
git commit -m 'Add my feature (attempt 5)' && \
git log --oneline --graph --abbrev=4
This command does the following:
- delete any current repo files (so I can re-run this command)
- setup the repo the way I want for testing and
- display the Git history that is created
When the command finishes the git history of the repo looks something like this:
* 87c6 (HEAD -> feat/myfeature) Add my feature (attempt 5)
* 1816 Add my feature (attempt 4)
* 4b3f Add my feature (attempt 3)
* 3282 Add my feature (attempt 2)
* 070f Add my feature (attempt 1)
* 3e62 (main) Initial commit: add README.md
Now when I run
git -c sequence.editor="vim --clean -c 'silent 2,\$s/^pick/squash/|:wq'" rebase --interactive --keep-base HEAD~5
The git interactive rebase screen (where I would change pick to squash) is skipped and we go directly to the screen where I have all 5 commit messages and I can combine them as desired. Once that is saved, running git log --oneline --graph --abbrev=4 looks like
* 9b4a (HEAD -> feat/myfeature) Add my feature
* 3e62 (main) Initial commit: add README.md
Success!
How this Works
At the heart of this command is
git rebase --interactive --keep-base HEAD~5
which drops you into your chosen editor with content that looks like
pick 070fb05 Add my feature (attempt 1)
pick 3282a75 Add my feature (attempt 2)
pick 4b3f39c Add my feature (attempt 3)
pick 18160b0 Add my feature (attempt 4)
pick 87c6d32 Add my feature (attempt 5)
To do this manually, we would update attempts 2 through 5 from pick to squash.
--keep-base
We include –keep-base on our git rebase call, so squash commits on our current branch without pulling in any new commits from our other branch (e.g. main). Typically, a git rebase -i main would both:
- Add any new commits that have been added to
main since we last updated from there
- Open the editor with the interactive rebase screen
By using --keep-base, we skip the first step and only do the second.
Automating
pick to
squash
Git has lots of configuration values and one of the things you can configure is what editor to use. I started by modifying the editor via the core.editor configuration value, but unfortunately an interactive rebase opens the editor twice (once to update the commits from pick to something else and once to craft the commit message for your new commit created by the squash). In this case, I want to automate the first but NOT automate the second.
The good news is Git version 2.40 (released in 2023) added a new configuration value, sequence.editor, that impacts when the editor is opened to update commits from pick to something else but NOT when crafting the commit message (see Use a Different Editor for Git Interactive Rebase).
We want to update this sequence.editor value but we only want to do this when we run this command (we don’t want to impact other times a Git interactive rebase is run). We can modify our Git config values just for the current command by run by using the Git -c option to set sequence.editor just for this run.
We’re going to set Git to use Vim as the Git interactive rebase editor (but we’re also going to pass some configuration values to Vim).
vim --clean -c 'silent 2,\$s/^pick/squash/|:wq'
--clean
The --clean argument results in “initializations from files and environment variables is skipped”.
We don’t want any of our custom Vim configuration or plugins impacting this code.
See :help --clean
-c {command}
The {command} given will run after Vim has loaded the file. Multiple commands can be given separated by a vertical bar (|). See :help -c
In this case, our command is
silent 2,$s/^pick/squash/|:wq
The 2,$s/^pick/squash/ command replaces pick with squash starting on line 2 and going to the end of the file ($) (see :help substitute) – this only applies on lines where “pick” appears at the beginning of the line (^). We prefix this command with silent to avoid the execution halting with the message
Press ENTER or type command to continue
The :wq, writes and quits the file (see :help :wq).
Peek Under the Hood
You can omit |:wq from the command to pause after the substitution has completed. If you run this command, you’ll need to enter :wq manually to get things moving again.
git -c sequence.editor="vim --clean -c 'silent 2,\$s/^pick/squash/'" rebase --interactive --keep-base HEAD~5
Adding this to My Git Configuration
Here is the PR where I add the git squashbackto alias to my Git configuration.
Other Solutions
In looking for this solution I found a number of other articles about this same idea of squashing the last so many commits but they all seem to use one of these two approaches:
- A Git interactive rebase (without the additional automation in this post), which has the disadvantage of requiring you update
pick to squash on lines you want to squash and then save this “sequence” file
- A Git reset to unstage the changes and then re-adding the changes and creating a new commit, which has the disadvantage of losing your previous commit messages