Merge, squash & rebase on GitHub - pros & cons

zdybit

Kacper Rychel

Posted on March 15, 2023

Merge, squash & rebase on GitHub - pros & cons

In the previous post, we got to know three ways to close pull requests on GitHub. Their different outcomes for the git history are more accurate for specific scenarios and branching strategies than others. All the closing options have their pros* and cons*. Let’s focus on them in this post and learn how to choose the closing options.

Catches explanation
Even if I list some content under ✅ & ❌ bullet points below, it doesn't mean ✅s are always good and ❌s are always bad. You can consider them more like desired outcomes, and their consequences we need to be aware of.
So yes, the pros and cons phrase here is kind of clickbait 😎

Table of content

Pros and cons

Pardon. Desired outcomes & consequences.

Create a merge commit

Since the Create a merge commit option keeps all commits and branches, it is the easiest way to synchronize branches with each other in both directions. Conflicts between branches, of course, can appear. They are resolved within a new merge commit, so no changes should be lost. It is as easy as it can be, but this easiness can be ugly and messy when we look at the git history.

So, what exactly is wrong with the simple and easy merge?

❌ It generates a new commit which does not provide any meaningful changes. It just merges the history of both branches. Finally, having two branches and the extra commit, the result is not the cleanest we could imagine. Even if a team (or an automated rule) removes all feature branches right after their pull requests are closed, the history of the feature branches remains, and the entire history looks like rails in the train barn.

Rails in the train barn

❌ Bad commit messages are the second awful thing in the "simple merge". We often don't bother about quality of them while we are working on our feature branches (I will call such commits working commits below). It is fine unless such commits are not merged into a long-living branch, like main or develop. Unfortunately, they are merged too often, and they stay there forever.

Commit messages mess

It is true that bad commit messages are a concern regardless of the way of the pull request closing, but the merge and rebase options preserve all commits in long-living branches.

TIP 💡
I heartily recommend reading the article about How to Write a Git Commit Message.
The approach, described in the article, makes my commits more descriptive and enforces me to think over what I commit. Even when I am still working on my feature branch with my working commits. It taught me discipline.

❌ Even if all contributors wrote really good commit messages, merge commits would still be interspersed with those from deleted (or not yet) feature branches. The commits from the different branches can be even more mixed together in the flatten view of the history on GitHub.

Mixed commits from different branches

It is tough to recognize which ones belong to which branches or tasks unless we study them in the full git tree.

✅ On the other hand, merge option preserves the entire history if you really need to have such.

✅ The most important benefit of the merge option is that it does not rewrite the history. Rewriting history is especially annoying when you need to compare two branches with the same changes from the same commits, but when the commits have been rewritten.

✅ You can create a next merge commit from the same source branch with new commits to the same target branch (e.g. from develop to main). Changes already merged before are not compared once again.

CONCLUSION 👁️‍🗨️
The Create a merge commit option is the best option for merging code between two long-living branches.

Squash and merge

Using the Squash and merge option, we can simply rephrase our entire work that we did on a feature branch. That way we get rid of multiple working commits with not always meaningful messages. The outcome is one elegant commit.

TIP 💡
Such commit message (according to How to Write a Git...) should contain a short descriptive title (what), an optional body explaining changes (why, not how), and a reference to a task ID and/or a pull request.

But what if there was so much to describe and so many changes to compare in one commit? Would it still be elegant?

First things first, when there is too much on commit's plate, it is the forceful signal to distribute changes to multiple commits or/and to split a task into smaller tasks or subtasks.

TIP 💡
A suitable candidate, what we can extract to a separate subtask, is refactor. If your team collects more and more refactoring tasks, I recommend reading the great article about A smooth way to pay your technical debt.

❎ As we rephrase our work during the squash of working commits, we are more likely to catch whether we have made too many changes than during the simple Create a merge commit option.

To give the devil his due, if we fill the pull request descriptions honestly, the chances to catch it will be as high. And even squashes can be made inattentively committing only default messages.

I call taking care of writing good commit descriptions a git hygiene.

TIP 💡
Fill pull requests fields honestly as well.
First, it is a message for your colleagues reviewing your job.
Second, default commit messages on GitHub can be generated from the pull request description and title.

Write descriptive messages of every commit which is supposed to be a part of the pull request.
Exactly for the same reason.

I described default messages generated by GitHub in the previous post Default messages of merge and squash.

End of digressions...
... for now 😉

❌ There is a risk of losing precious information from the git history when a feature branch is removed after it is squashed to a target branch. We need to carefully read messages we want to rephrase.

❌ Have you ever tried to squash the same branch twice?

Let's see the scenario when the 71670e04 commit from the feature/7 has been squashed into the main (f489ebfc), but we pushed another commit into the feature/7 (f4c25692).

A Git tree before the squash

We want to squash the new commit to the main.

The conflict view in VS Code

Yes, there is a conflict, even if a content has been added to the end of the file in the feature/7 and nothing has been changed in the main. Nobody touched the main after our last squash to it. The conflict occurred due to lack of relation between those two branches, so git doesn’t know which change precedes.

Let's take a look what will happen on GitHub when we raise a pull request for the feature/7 :

A conflict in the pull request

There are our expected conflict and two commits to merge.

Wait! What? Why are there two commits when the first of them has been squashed into the main already?

What's more, our conflict is even more misleading on GitHub:

The conflict view on GitHub

This all happens due to the feature/7 and the main have nothing in common after the 93eda5e0 commit. Please remember.

CONCLUSION 👁️‍🗨️
The Squash and merge option is not suitable for using with two long-living branches.
It is the great option to close pull requests of feature branches.

Does it mean Squash and merge is a bad option?

Not at all.

✅✅ Thanks to the squash, we can keep the git history spotless and readable. There are no merge commits which tell us nothing meaningful. Especially when all feature branches are removed after their changes are put into a target branch. We just need to maintain the git hygiene 😷 and not forget about references in the commit messages.

Squashed commits in the GH history

Squashed commits in the git tree

It is much more readable now than when we merged it with the merge commits. We know immediately what and when specific changes have been added to the main.

The abde78a2 commit squashed all commits from the feature/10, both the working commits and the merge commits. If we wanted to keep the commits from the feature/9, it would be better to squash them separately into the main and resolve potential conflicts.

Rebase and merge

Clean your feature

Alternatively, we could drop the merge commits by rebasing the feature/10 locally before we create a pull request and rebase new clean commits into the main by the Rebase and merge closing option of the pull request. Even if we planed to close the pull request with Create a merge commit option, we still could rebase all working commits locally to keep only one well described commit.

✅✅ Rebase is a great tool to clean our feature branch before we raise a pull request. Such rebasing on local is pretty complex and offers many options, but it brings great effects as a final point. It allows us to shape our feature branches to the state we are proud to share within a pull request.

TIP 💡
You can learn more about the most commonly used git commands with the great article: 🌳🚀 CS Visualized: Useful Git Commands. You can also find the explanation with visualizations for Interactive Rebase and Resetting what help you to clean a feature up.

GitHub rebase

Despite the fact the previous section does not deal with the post topic directly, it still is important. Especially when we are going to close our pull request with the Rebase and merge option. All commits from a feature branch are copied into the main. And then it is too late to clean up the commits.

WARNING ⚠️
Never git rebase on public branches, like main or develop.
When somebody had created its feature branch from a commit that has been removed from the origin main, the feature branch is cut off from the origin main as well. The removed commits exist only on the local main. All of them will be considered as new commits during a pull request to the origin main. And that brings vast number of conflicts! Even if such commits contain the same messages and changes, they have the different signatures than the original commits.

Rebasing public branches causes huge trouble to clean the origin up and continue working with the same common code base.

Ok then. Rebase is difficult and dangerous. So, should we avoid using it?

Not really. I strongly encourage learning git rebase, use it locally, and use GitHub's Rebase and merge option when necessary.

Rebase and merge option is pretty similar to the Squash and merge one. It helps us to keep the git history clean and readable without merge commits. The difference is, rebase can add more than one commit into the main.

We just need to maintain the git hygiene and do not forget about references in the commit messages.

❌ This time, in contrast to the squash option, we need to remember adding the references to the messages of feature commits before we can close a pull request. There is no place to edit commit messages within the pull request, like with the squash option where it is possible.

❌ Since commits are copied to a target branch, there is no relation between the source and the target branches. It raises the possibility of having conflicts between changes which are already existing on the both branches. The same case as with the squash option.

CONCLUSION 👁️‍🗨️
The Rebase and merge option is not suitable for using with two long-living branches.
It is the good option to close pull requests of feature branches when you want to keep multiple commits.

A little bit of practice

Probably, the most commonly used branching strategy is Gitflow workflow. It is also one of the most complex strategy. So, let's try to study such case.

As we have learned already, the merge option is suitable to multiple long-live branches like main and develop.

On the other hand, the squash and the rebase options make an excellent job when we want to move changes from a branch which is about to be deleted right after its pull request is closed.

Gitflow strategy

In the picture above, the hotfix is merged into both the main (1) and the develop (2). If the hotfix were squashed or rebased, the same change from the hotfix would be a conflict to itself in the pull request from the release to the main (3).

On the other hand, let see at the develop and feature branches (4). Imagine there are even more feature branches developers are working on in parallel. So, what can we do to avoid the rails in the train barn between the develop and features?

First thing, keep features clean and write good commit messages.

STATEMENT
I know I keep talking about writing good commit messages over and over. I do because they are really that important, and very often neglected by developers. We all are aware of the quality of code and documentation.
And what are commit messages if not the code and the documentation?

The second thing, we can squash features into the main, or rebase them if we would like to keep multiple commits of them.

Summary

We've just learnt more about the outcomes of the options to close pull requests on GitHub, and what consequences they raise. Now, we can handle our pull requests more effectively and wisely. We are not afraid of other options than Create a merge commit anymore.

In the series we learnt what we could use, and when we would have used them.
In the next post we will get to know how GitHub uses git under the hood to give us the discussed outcomes. We will dive a little bit into git commands.

💖 💪 🙅 🚩
zdybit
Kacper Rychel

Posted on March 15, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related