A quick guide to squashing Git commits
Tags: git, devtips, devops
Published: 2018-08-06 13:50:27

---
title: A quick guide to squashing Git commits
published: true
description: Three methods for cleaning up your Git history.
tags: ["git","devtips","devops"]
cover_image: https://thepracticaldev.s3.amazonaws.com/i/0ffx50ka03rvy3epax5t.png
---

Let's get one thing out of the way first: rewriting Git history just for the sake of having a pretty tree, especially with public repositories, is generally not advisable. It's kind of like going back in time, where changes you make to your version of the project cause it to look completely different from a version that someone else forked from a point in history that you've now erased - I mean, haven't you seen _Back to the Future Part II_? (If you'd rather maintain that only one _Back to the Future_ movie was ever made, thus sparing your future self from having to watch the sequels, I get it.)

Here's the main point. If you've pushed messy commits to a public repository, I say go right ahead and leave them be, instead of complicating things further. (We all learn from our embarrassments, especially the public ones - I'm looking at you, past-Victoria.) If your messy commits currently only exist on your local version, great! We can tidy them up into one clean, well-described commit that we'll be proud to push, and no one will be the wiser.

There are a couple different ways to squash commits, and choosing the appropriate one depends on what we need to achieve.

The following examples are illustrated using `git log --graph`, with some options for brevity. We can set a handy alias to see this log format in our terminal with:

```sh
git config --global alias.plog "log --graph --pretty=format:'%h -%d %s %n' --abbrev-commit --date=relative --branches"
```

Then we just do `git plog` to see the pretty log.

# Method #1: one commit to rule the master branch

This is appropriate when:

* We're committing directly to master
* We don't intend to open a pull request to merge a feature 
* We don't want to preserve history of branches or changes we haven't yet pushed

This method takes a Git tree that looks like this:

```console
*   3e8fd79 - (HEAD -> master) Fix a thing
|
*   4f0d387 - Tweak something
|
*   0a6b8b3 - Merge branch 'new-article'
|\
| * 33b5509 - (new-article) Update article again again
| |
| * 1782e63 - Update article again
| |
| * 3c5b6a8 - Update article
| |
* | f790737 - (master) Tweak unrelated article
|/
|
* 65af7e7 Add social media link
|
* 0e3fa32 (origin/master, origin/HEAD) Update theme
```

And makes it look like this:

```console
* 7f9a127 - (HEAD -> master) Add new article
|
* 0e3fa32 - (origin/master, origin/HEAD) Update theme
```

Here's how to do it - hold on to your hoverboards, it's super complicated:

```sh
git reset --soft origin/master
git commit
```

Yup that's all. We can delete the unwanted branch with `git branch -D new-article`.

# Method #2: not _that_ much!

This is appropriate when:

* We want to squash the last _x_ commits but not _all_ commits since `origin/master`
* We want to open a pull request to merge a branch

This method takes a Git tree that looks like this:

```console
* 13a070f - (HEAD -> new-article) Finish new article
|
* 78e728a - Edit article draft
|
* d62603c - Add example
|
* 1aeb20e - Update draft
|
* 5a8442a - Add new article draft
|
| * 65af7e7 - (master) Add social media link
|/
|
* 0e3fa32 - (origin/master, origin/HEAD) Update theme
```

And makes it look like this:

```console
* 90da69a - (HEAD -> new-article) Add new article
|
| * 65af7e7 - (master) Add social media link
|/
|
* 0e3fa32 - (origin/master, origin/HEAD) Update theme
```

To squash the last five commits on branch `new-article` into one, we use:

```sh
git reset --soft HEAD~5
git commit -m "New message for the combined commit"
```

Where `--soft` leaves our files untouched and staged, and `5` can be thought of as "the number of previous commits I want to combine."

We can then do `git merge master` and create our pull request.

# Method #3: getting picky

Say we had a really confusing afternoon and our Git tree looks like this:

```console
* dc89918 - (HEAD -> master) Add link
|
* 9b6780f - Update image asset
|
* 6379956 - Fix CSS bug
|
*   16ee1f3 - Merge master into branch
|\
| |
| * ccec365 - Update list page
| |
* | 033dee7 - Fix typo
| |
* | 90da69a - Add new article
|/
|
* 0e3fa32 - (origin/master, origin/HEAD) Update theme
```

We want to retain some of this history, but clean up the commits. We also want to change the messages for some of the commits. To achieve this, we'll use `git rebase`.

This is appropriate when:

* We want to squash only some commits
* We want to edit previous commit messages
* We want to delete or reorder specific commits

Git `rebase` is a powerful tool, and handy once we've got the hang of it. To change all the commits since `origin/master`, we do:

```sh
git rebase -i origin/master
```

Or, we can do:

```sh
git rebase -i 0e3fa32
```

Where the commit hash is the last commit we want to retain as-is.

The `-i` option lets us run the interactive rebase tool, which launches our editor with, essentially, a script for us to modify. We'll see a list of our commits in reverse order to the git log, with the oldest at the top:


```console
pick 90da69a Add new article
pick 033dee7 Fix typo
pick ccec365 Update list page
pick 6379956 Fix CSS bug
pick 9b6780f Update image asset
pick dc89918 Add link

# Rebase 0e3fa32..dc89918 onto 0e3fa32 (6 commands)
#
# Commands:
# p, pick = use commit
# r, reword = use commit, but edit the commit message
# e, edit = use commit, but stop for amending
# s, squash = use commit, but meld into previous commit
# f, fixup = like "squash", but discard this commit's log message
# x, exec = run command (the rest of the line) using shell
# d, drop = remove commit
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
# Note that empty commits are commented out
#
~
```

The comments give us a handy guide as to what we're able to do. For now, let's squash the commits with small changes into the more significant commits. In our editor, we change the script to look like this:

```console
pick 90da69a Add new article
squash 033dee7 Fix typo
pick ccec365 Update list page
squash 6379956 Fix CSS bug
squash 9b6780f Update image asset
squash dc89918 Add link
```

Once we save the changes, the interactive tool continues to run. It will execute our instructions in sequence. In this case, we see the editor again with the following:

```console
# This is a combination of 2 commits.
# This is the 1st commit message:

Add new article

# This is the commit message #2:

Fix typo

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# interactive rebase in progress; onto 0e3fa32
# Last commands done (2 commands done):
#    pick 90da69a Add new article
#    squash 033dee7 Fix typo
# Next commands to do (4 remaining commands):
#    pick ccec365 Update list page
#    squash 6379956 Fix CSS bug
# You are currently rebasing branch 'master' on '0e3fa32'.
#
# Changes to be committed:
#       modified:   ...
#
~
```

Here's our chance to create a new commit message for this first squash, if we want to. Once we save it, the interactive tool will go on to the next instructions. Unless...

```console
[detached HEAD 3cbad01] Add new article
 1 file changed, 129 insertions(+), 19 deletions(-)
Auto-merging content/dir/file.md
CONFLICT (content): Merge conflict in content/dir/file.md
error: could not apply ccec365... Update list page

Resolve all conflicts manually, mark them as resolved with
"git add/rm <conflicted_files>", then run "git rebase --continue".
You can instead skip this commit: run "git rebase --skip".
To abort and get back to the state before "git rebase", run "git rebase --abort".

Could not apply ccec365... Update list page
```

Again, the tool offers some very helpful instructions. Once we fix the merge conflict, we can resume the process with `git rebase --continue`. Our interactive rebase picks up where it left off.

Once all the squashing is done, our Git tree looks like this:

```console
* 3564b8c - (HEAD -> master) Update list page
|
* 3cbad01 - Add new article
|
* 0e3fa32 - (origin/master, origin/HEAD) Update theme
```

Phew, much better.

_This post was lovingly ripped out of a longer article [on my blog about Git commit practices.](https://victoria.dev/verbose/git-commit-practices-your-future-self-will-thank-you-for/)_