Kacper Rychel
Posted on May 16, 2023
We are engineers for many reasons. The common one is we want to know, how it works and how it is made. In this article, I will feed your curiosity hunger for both how it works, showing you how GitHub closes pulls request under the hood, and how it is made, playing with Git commands to reproduce the same result. On top of that, I made a practical brief in fast-forward (--ff
, --no-ff
, --ff-only
) merge strategies in practice. Two birds with one stone ๐ฆ๐ฆ
Note ๐ฌ
If you are not interested in details of reproducing the GitHub's pull requests results, you can go straight to the TLDR section, but I do not recommend missing out on the fun ๐
Let's start the adventure.
Table of content
- Table of content
-
--ff
, or--no-ff
, orff-only
, that is that question - Closing Pull Request in practice
- TLDR
- Ending
--ff
, or --no-ff
, or ff-only
, that is that question
GitHub merges branches with the --no-ff
or --ff-only
strategy depending on the closing options.
--ff
(fast forward) and --ff-only
(fast forward only) simply move a pointer forward. They vary when fast forward is not possible. Then, --ff
switches to --no-ff
, and --ff-only
rejects the operation.
--no-ff
(no fast forward) just creates a merge commit.
--ff
is a default strategy for git pull
and git merge
commands.
fast forward in practice
Now, let's see how it works in practice with the initial state:
$ git pull origin feature/4
From https://github.com/zdybasny/git-merges-strategies
* branch feature/4 -> FETCH_HEAD
Updating 6cf021a..fa923ee
Fast-forward
README.md | 4 ++++
1 file changed, 4 insertions(+)
$ git push
See? Fast-forward was used by default, even if no flag was provided.
The result:
Here is a sample scenario, which fast forward is not possible for:
$ git pull origin feature/5 --ff-only
From https://github.com/zdybasny/git-merges-strategies
* branch feature/5 -> FETCH_HEAD
fatal: Not possible to fast-forward, aborting.
The operation was aborted for the --ff-only
strategy.
$ git pull origin feature/5 --ff
From https://github.com/zdybasny/git-merges-strategies
* branch feature/5 -> FETCH_HEAD
Merge made by the 'ort' strategy.
new-file.md | 3 +++
1 file changed, 3 insertions(+)
create mode 100644 new-file.md
For the --ff
strategy, the operation switched to the --no-ff
one and created a merge commit:
Bonus puzzle
Being familiar with the fast forward strategy, you can try yourself with a simple puzzle.
What will the command below do for feature/5
branch in current state:
-
git pull origin main --no-ff
?
โ
โ
โ
Imagine more such merges between more branches ๐ฑ
-
git pull origin main --ff
?
โ
โ
โ
Much cleaner ๐คฉ
Unfortunately, GitHub supports only --no-ff
merges, what you will see in the next part of the article.
Closing Pull Request in practice
Create a merge commit
Before starting to play with reproducing the outcome of *Create a merge commit`, let's see what GitHub's documentation says about it:
When you click the default Merge pull request option on a pull request on GitHub.com, all commits from the feature branch are added to the base branch in a merge commit. The pull request is merged using the --no-ff option.
The feature branch here means the source branch, and the base branch means the target branch.
And this is the result from Github to reproduce:
Knowing the theory and what is to do, we can use following Git commands:
$ git checkout main
$ git pull origin feature/3 --no-ff
$ git push
There are two things to notice:
- Even the green branch has been removed, its history still exists. The same will happen if we remove the
feature/3
branch. This is exactly the result of the--no-ff
strategy ofgit pull
andgit merge
. - Default messages generated by GitHub and by Git differ.
- The message generated by Git doesn't include a PR's number. We can still write our own merge commit message and attach a PR reference to it. We must add the
-m
flag to do so. - I described default messages from GitHub in the first post of this series.
- The message generated by Git doesn't include a PR's number. We can still write our own merge commit message and attach a PR reference to it. We must add the
As you see, the results are pretty the same, so you know what happens behind the scenes of GitHub when it merges your PR.
Squash and merge
GitHub's documentation says about Squash and merge:
When you select the Squash and merge option on a pull request on GitHub.com, the pull request's commits are squashed into a single commit. Instead of seeing all of a contributor's individual commits from a topic branch, the commits are combined into one commit and merged into the default branch. Pull requests with squashed commits are merged using the fast-forward option.
The documentation says that the fast-forward strategy is used for squashing. From Git point of view, --squash is the flag of git merge
. The statement seems to be informative only, because:
$ git merge origin feature/6 --squash --no-ff
fatal: You cannot combine --squash with --no-ff.
Here is the state to reproduce:
Git CLI commands:
$ git checkout main
$ git merge origin feature/7 --squash
--ff
flag could be skipped in the command above because it is the default strategy.
$ git commit
We could also use -m
flag to write a message in a terminal inline, but writing it in an editor is more convenient and a there is a default message to edit already.
$ git push
The results look the same but the default commit messages.
Rebase and merge
Rebase is a powerful tool to rewriting a Git history, and its description in the documentation is pretty complex and focused on vary cases. You can just read it on your risk ๐ here: https://git-scm.com/docs/git-rebase. Just kidding, it is worth to read it.
GitHub rebases in only one way, so its documentation is much clearer:
When you select the Rebase and merge option on a pull request on GitHub.com, all commits from the topic branch (or head branch) are added onto the base branch individually without a merge commit. In that way, the rebase and merge behavior resembles a fast-forward merge by maintaining a linear project history. However, rebasing achieves this by re-writing the commit history on the base branch with new commits.
The rebase and merge behavior on GitHub deviates slightly from Git rebase. Rebase and merge on GitHub will always update the committer information and create new commit SHAs, whereas Git rebase outside of GitHub does not change the committer information when the rebase happens on top of an ancestor commit
The state to reproduce:
This time, it is not enough to run one or two Git commands to achieve the same result.
Long story short, we need to:
- rebase the
feature/9
branch onto themain
- move the
main
pointer tofeature/9
- reset
feature/9
toorigin/feature/9
So, let's do it step by step.
First, we rebase the feature/9
branch onto the main
branch with the flag --force-rebase
to achieve a similar state on your local:
$ checkout feature/9
$ git rebase --force-rebase --onto main main feature/9
Here is the difference, which GitHub says in its documentation about. The feature/9
is rebased onto main
, but GitHub doesn't touch the source branch. So, you don't as well. You need to just keep the origin/feature/9
as it is. To do so, we need to move the main
pointer to feature/9
and reset feature/9
to origin/feature/9
. To move the pointer, we will use the fast-forward strategy we have already learned:
$ git checkout main
$ git merge feature/9 --ff-only
$ git push
And now the reset of feature/9
:
$ git checkout -
$ git reset HEAD~2 --hard
$ git pull
git checkout -
is a shortcut for git checkout @{-1}
. It is useful when you need to switch to the previous branch.
The reset command above moved the branch pointer back for 2 commits (HEAD~2
) and dropped their changes (--hard
).
git pull
is for updating the local feature/9
branch to the state of the remote origin/feature/9
branch.
Voilร ! ๐ The result is the same as we did it by using the GitHub's UI.
TLDR
Create a merge commit:
$ git checkout main
$ git pull origin feature/source-branch --no-ff
$ git push
Squash and merge:
$ git checkout main
$ git merge origin feature/source-branch --squash --ff
$ git commit
$ git push --force
Rebase and merge:
$ git checkout feature/source-branch
$ git rebase --force-rebase --onto main main feature/source-branch
$ git checkout main
$ git merge feature/source-branch --ff
$ git push
$ git checkout -
$ git reset HEAD~2 --hard
$ git pull
Ending
I hope you enjoyed our play with Git and GitHub. Asking yourself questions like "How does it work?" and "How can I do it by myself?" is a wonderful way to learn something new. Very often, something we have never thought about before, like me before when I started to draft this article.
This article is the last part of the series, within which I wanted to share with you what options of closing pull requests on GitHub are, when to use them, and now how to do it by yourself. I hope you have found it useful and interesting.
Posted on May 16, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.