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.
❌ 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 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.
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 👁️🗨️
TheCreate 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, nothow), 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
).
We want to squash the new commit to the main
.
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
:
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:
This all happens due to the feature/7
and the main
have nothing in common after the 93eda5e0
commit. Please remember.
CONCLUSION 👁️🗨️
TheSquash 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.
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 ⚠️
Nevergit rebase
on public branches, likemain
ordevelop
.
When somebody had created its feature branch from a commit that has been removed from theorigin main
, the feature branch is cut off from theorigin main
as well. The removed commits exist only on thelocal main
. All of them will be considered as new commits during a pull request to theorigin 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 👁️🗨️
TheRebase 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.
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.
Top comments (0)