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:



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:



* 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:



* 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:



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:



* 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:



* 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:



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:



* 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:



git rebase -i origin/master

Or, we can do:



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:



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:



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:



# 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...



[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:



* 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.