DEV Community

Cover image for How to squash commits in a PR
YJDoc2 for DJ Unicode

Posted on

How to squash commits in a PR

Helped by Palka , Parth, and Yatharth.
Header image created by Saloni. Git illustration in text created by Milan


Hello everyone !
As you might be aware, hacktoberfest is approaching, and in about a month many of you will be helping out on open source projects. Of course, working on projects is a side effect, you know why we really do it...😏😏 To plant a tree, obviously 🌳🌲🌳🌲 (Seriously, we should do that seeing the amount of wildfires this year)

Anyway, while making PRs, you might be asked to squash formatting or spelling error commits, and to save you a Google search, I am writing this post to show you why, when and how to squash commits in your PR.

Why would you ever want to squash something???

So imagine this, you have written your beautiful code, written comments in a well known format, the code is compiling and working, and you satisfyingly make a commit with the perfect commit message.... Only to realize later that you have made a spelling error πŸ₯²πŸ€¦ Now you can use a soft reset, fix the error, and re-commit, leaving no one any wiser, but what if you had already pushed the commit in PR?

More realistically, imagine you're working on a PR, and as you're pushing the commits, and they're getting reviewed you accidentally mess up formatting and commit. Or along an ongoing discussion in a PR, you have made some patch commits and now before merging the PR you want to combine them, as old patches don't really make sense . This is where squashing the commits would come handy.

Squashing allows to combine or 'squash' the commits together, leaving a nice git history behind. That way you can combine all your patch commits into a single commit, or squash formatting and spelling error commits into other commits, leaving behind a git history of only necessary commits.

Git squash example

When should the squashing be done?

Now on an important note, you should not squash commits if they're pushed to the main branch of the repository. Squashing changes git history, which means that history of all forks of that branch will be inconsistent with your repository, giving everyone a lot of merge conflicts, and you know how you were when you got conflicts, right πŸ˜‰ πŸ˜‰

However if you're working on a branch which is not main for an issue, and you intend to make a PR to the main branch, it is totally fine to squash the commits. As most of the times you alone are working on that specific branch, even if the history changes it will not affect anyone else. Then once everything is done, you can merge the PR without any history conflicts.

How to squash commits

Let us see with an example. Consider a project on which you're working, that already has some commits on its main branch.

# run git log --pretty=oneline
7dc92d4...(HEAD -> Main) Add ReadMe
d3395b0... Initial Commit
Enter fullscreen mode Exit fullscreen mode

Now to grow some trees 🌲🌳 you decide to work on this project and help in solving issue-1. So you fork and clone the project, make a branch named issue-1 like a good contributor and start working on the issue.

You add feature 1 and feature 2 in their own commits and make a PR. After discussing with the reviewers, you find that there was a bug in your commits and you make a commit to fix it.

Now, your git history looks like this

# git log --pretty=oneline
b2f4c83... (HEAD -> issue-1) Bug Fix
32e414f... Add Feature 2
7654633... Add Feature 1
7dc92d4... (Main) Add ReadMe
d3395b0... Initial Commit
Enter fullscreen mode Exit fullscreen mode

You have your main branch's head on Add Readme commit, and issue-1's head on Bug Fix commit.

Now as you're about to start with feature 3 , when someone comments on your PR, noticing a formatting and spelling error😭😒
Ah well.... You fix those, make a commit and start working on the feature 3. After finally making if work, your commit history now looks like this

# git log --pretty=oneline
f592171... (HEAD -> issue-1) Add Feature 3
574b345... Fix Formatting
b2f4c83... Bug Fix
32e414f... Add Feature 2
7654633... Add Feature 1
7dc92d4... (Main) Add ReadMe
d3395b0... Initial Commit
Enter fullscreen mode Exit fullscreen mode

As you push these to your repository, and wait eagerly to get it merged, someone asks you to squash the formatting commit, so the main repository history would look nice.

Now to squash the commits, you need to run

git rebase -i HEAD~4
Enter fullscreen mode Exit fullscreen mode

The -i option make the rebase interactive, and HEAD~4 considers last 4 commits from current Head position for the rebase. If your formatting commit was 5 commits before current, you would replace 4 with 6 or some numbers greater than 5, to get all commits till there.

After running this, you will see a message like

pick 32e414f Add Feature 2
pick b2f4c83 Bug Fix
pick 574b345 Fix Formatting
pick f592171 Add Feature 3

# Rebase 7654633..f592171 onto f592171 (4 commands)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup <commit> = like "squash", but discard this commit's log message
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
# .       create a merge commit using the original merge commit's
# .       message (or the oneline, if no original merge commit was
# .       specified). Use -c <commit> to reword the commit message.
#
# 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

Enter fullscreen mode Exit fullscreen mode

This shows first, the commits : from oldest to latest, and then available options, such as squash, edit, fixup... Git can be really powerful once you get to know it... But I don't know much of it other than handful of commands, so let us get on with squashing the formatting commit.

To perform an action on a commit, you change the word 'pick' before that commit to that action, as described in comments. To squash the formatting commit, you change it as

pick 32e414f Add Feature 2
pick b2f4c83 Bug Fix
squash 574b345 Fix Formatting
pick f592171 Add Feature 3
Enter fullscreen mode Exit fullscreen mode

After saving this you will get another commit message, showing commit message before the squashed commit, and the squash commit message.

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

Bug Fix

# This is the commit message #2:

Fix Formatting

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# Date:      Thu Aug 19 19:03:51 2021 +0530
#
# interactive rebase in progress; onto 7654633
# Last commands done (3 commands done):
#    pick b2f4c83 Bug Fix
#    squash 574b345 Fix Formatting
# Next command to do (1 remaining command):
#    pick f592171 Add Feature 3
# You are currently rebasing branch 'issue-1' on '7654633'.
#
# Changes to be committed:
#       modified:   readme.md
#
Enter fullscreen mode Exit fullscreen mode

In case the commit was something like a patch in series where you want to keep the message, this will allow you to do that. Otherwise if you just want to forego the message, you can also use the fixup instead of squash. Here you can comment the second commit message, and save, like usual git commit.

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

Bug Fix

# This is the commit message #2:

# Fix Formatting

# Please enter the commit message for your changes. Lines starting
...
Enter fullscreen mode Exit fullscreen mode
# git log --pretty=oneline
29768cb... (HEAD -> issue-1) Add Feature 3
10c38d8... Bug Fix
32e414f... Add Feature 2
7654633... Add Feature 1
7dc92d4... (Main) Add ReadMe
d3395b0... Initial Commit
Enter fullscreen mode Exit fullscreen mode

Ah, now you have a clean git history, alas, if you try to push this to your repository branch, you will get error saying cannot push because histories are different. Your repository has the formatting commit whereas your local has no such thing in its history. To fix this you'll have to force push :

git push -f origin issue-1:issue-1
# as you're on issue-1 branch, you can leave the last part, but I like the extra confirmation
Enter fullscreen mode Exit fullscreen mode

With this your PR will be updated, hopefully merged, and you'll be one step closer to planting that plant 🌱


Hope you found this useful, and may your PRs be clean :)

Since I started this post talking about hacktoberfest, let me end with the same. Do you have any repository that you plan to open up for contributing to this hacktoberfest? Sometimes it is hard to make your repository visible in all repos open for contributing, so this is kind of an early call... If you have any interesting repo you would like to open for contribution, post them in the comments. Let's start planting the trees. 🌱🌱🌱

Oldest comments (2)

Collapse
 
guyrouillier profile image
Guy Rouillier

Thank you for this tutorial. I'm new to squashing, so want to make sure I understand it before I try it. After the rebase command, the system replies with a list of affected commits, followed by this message:

Rebase 7654633..f592171 onto f592171 (4 commands)

I'm confused because commit "7654633-Add feature 1" is not actually included in the list. So why is it included in the bounds?

Thanks.

Collapse
 
yjdoc2 profile image
YJDoc2

Hey, I cannot give you a good resource right now, but I'm pretty sure it's that way because the commit ranges exclude the first commit. So in the range specified by A...B, commit A is not actually considered, it considers commits after A, till B ; where B is inclusive. You can check git-revert or git-rebase man pages, which contains links for git commit ranges explanation. I'll also try to add link in another comment if I find any good explanation. Hope this helps.