loading...
Cover image for A quick guide to squashing Git commits

A quick guide to squashing Git commits

victoria profile image Victoria Drake Updated on ใƒป5 min read

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.

Posted on Mar 8 '18 by:

victoria profile

Victoria Drake

@victoria

Senior software engineer with open source track record. ๐Ÿ’œ Cybersecurity training, red team automation. Co-author, maintainer, OWASP Web Security Testing Guide. Made ApplyByAPI.com ๐Ÿ’Œ hello@victoria.dev

Discussion

markdown guide
 

I'm always use this line to merge from another branch:

git merge --squash feature_branch

If you have conflicts and you trust in the changes of feature_branch, I mean you know the changes in the feature_branch are the correct, you can tell to git that take this confidence changes over the master or another branch where you need to apply the merge:

git merge --squash feature_branch -X theirs

In the other side:

git merge --squash feature_branch -X ours

And continue with the normal flow....

This is called "Strategy option"

 

Curiosity: In the event you have two branches youโ€™ve been handed via a new project, which one can you trust as the source of truth? Typically Iโ€™ve trusted the most recent branch, but found that older branches have some better days than newer ones. Thoughts?

 

I look at the contents. In the event that I'm unable to reach a verdict which one is better, I trust neither

 

If you want some colors in the git plog display, update the --pretty flag with this instead:

git log --graph --pretty='%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit --date=relative --branches
 

rewriting Git history just for the sake of having a pretty tree, especially with public repositories, is generally not advisable

While I totally agree with this, your article has taught me some techniques that make me want to go rewrite my Git histories right now!

 

Thanks for the amazing article.

git commit --amend to add changes to the previous commit message. Hope you can give the better explanation on this.

 

Sure thing! Any other Git commands that everyone might like to see in plain English? I'll fold them into a future post. :)

 

If folks are using GitHub they can use squash-and-merge to merge a PR to get the โ€œgit merge โ€”squashโ€ behaviour. Then the PR lands as a single commit with two parents. On the branch you merged into the git log shoes a single clean commit. In the PR brach the git log shows all the original commits which can be noisy such as making minor changes due to code review.

 

So this squash thing got our production Admin JWT Token out of the public repository's history, so it is a cool reason to go ahead and fix your history.
Awesome article, that I will surely check back on to, when a squash will seem appropriate, of course.

 

Keep in mind, it's not the squash (you could have edited the borked commit alone and kept the rest).

Also, once published the blobs will be in all clones' object databases for some time (until garbage collect). So, in all honesty this seems like a bad fix for a security risk. Instead, just revoke the admin token and generate a brand-new one.

 

I use

git rebase -i origin/master

All the time.

Also you can give s for squash and p for pick. Just to save some time in typing

 

How do you fixed the merge conflicts?

 

git mergetool

Run that at the command line and it will launch whatever mergetool specified in your git config. You may need to install a mergetool first, some people use kdiff3, some hate it.

 

Nice tips!

Rewrite histories only on commits that have not been pushed to remote repo yet, and you must make sure that it will not cause troubles to others.

 
  • create new branch for feature/bug
  • edit, test, commit, repeat in branch
  • merge finished branch into master with --no-ff
  • delete feature/bug branch
  • history is clean