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.
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
- A little bit of practice
Pardon. Desired outcomes & consequences.
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.
❌ 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.
It is true that bad commit messages are a concern regardless of the way of the pull request closing, but the
rebase options preserve all commits in long-living branches.
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.
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
main). Changes already merged before are not compared once again.
Create a merge commitoption is the best option for merging code between two long-living branches.
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.
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.
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.
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.
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
f489ebfc), but we pushed another commit into the
We want to squash the new commit to the
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
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
What's more, our conflict is even more misleading on GitHub:
This all happens due to the
feature/7 and the
main have nothing in common after the
93eda5e0 commit. Please remember.
Squash and mergeoption 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.
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
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.
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.
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.
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.
git rebaseon public branches, like
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 mainas 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.
Rebase and mergeoption 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.
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
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.
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
On the other hand, let see at the
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
First thing, keep
features clean and write good commit messages.
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.
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.