Merge, squash & rebase on GitHub - pros & cons
Tags: github, git, tutorial, devops
Published: 2023-03-15 23:05:00

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

- [Table of content](#table-of-content)
- [Pros and cons](#pros-and-cons)
  - [Create a merge commit](#create-a-merge-commit)
  - [Squash and merge](#squash-and-merge)
  - [Rebase and merge](#rebase-and-merge)
     - [Clean your feature](#clean-your-feature)
     - [GitHub rebase](#github-rebase)
- [A little bit of practice](#a-little-bit-of-practice)
- [Summary](#summary)

## 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2oyhftpxdgoim7c8ozt1.png)

❌ 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sk7vtg3qemo3vuwjpdlm.png)

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](https://cbea.ms/git-commit/). 
> 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/a3yvajmbzfe1ergta7bi.png)

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...](https://cbea.ms/git-commit/)) 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](https://dev.to/adambrandizzi/billing-the-technical-debt-45kb).

❎ 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 <u>Default messages of [merge](https://dev.to/zdybit/3-options-to-close-pull-requests-on-github-what-2j3n#default-messages-of-merge) and [squash](https://dev.to/zdybit/3-options-to-close-pull-requests-on-github-what-2j3n#default-messages-of-squash)</u>.

*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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fo184x550gg655f2ugzx.png)

We want to *squash* the new commit to the `main`.

![The conflict view in VS Code](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mtf4lav8a1oziwzql05v.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pszwml5m9wbzc52socz9.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wtvldpanihglzbpyiufj.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/alxj7stevunjprkqvugs.png)

![Squashed commits in the git tree](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sn6owtnagbjqyb47d7zn.png)

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](https://dev.to/lydiahallie/cs-visualized-useful-git-commands-37p1). 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](https://www.atlassian.com/git/tutorials/comparing-workflows/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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ry8w1hizgeifbkf4vgfh.png)

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 `feature`s?

***First thing, keep `feature`s 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** `feature`s 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.