Learn Git fast forward by reproducing GitHub's merges in practice
Tags: git, github, devops
Published: 2023-05-16 10:04:52

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](#tldr) section, but I do not recommend missing out on the fun 😎

Let's start the adventure.

## Table of content

- [Table of content](#table-of-content)
- [`--ff`, or `--no-ff`, or `ff-only`, that is that question](#--ff-or---no-ff-or-ff-only-that-is-that-question)
  - [fast forward in practice](#fast-forward-in-practice)
  - [Bonus puzzle](#bonus-puzzle)
- [Closing Pull Request in practice](#closing-pull-request-in-practice)
  - [Create a merge commit](#create-a-merge-commit)
  - [Squash and merge](#squash-and-merge)
  - [Rebase and merge](#rebase-and-merge)
- [TLDR](#tldr)
- [Ending](#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:

![ff---tree-before-merge-ff](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/v74kocz1729hubn5erov.png)

```bash
$ 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:

![ff---tree-after-merge-ff](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ruuri9nvgxxdbcgeojw9.png)

Here is a sample scenario, which *fast forward* is not possible for:

![ff---tree-before-failing-merge-ff-only](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0ld9f8ye45beyy73st46.png)

```bash
$ 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.

```bash
$ 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:

![ff---tree-after-merge-ff-switched-to-no-ff](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8q0tu1gcwici0jtzio4a.png)

### 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:

![ff-bonus---current-tree](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/12w6nk5gwcfa52fem0i7.png)

- `git pull origin main --no-ff`?

⁝

⁝

⁝

![ff-bonus---merge-no-ff](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z45w2u3v1pwpino5g46t.png)

Imagine more such merges between more branches 😱

- `git pull origin main --ff`?

⁝

⁝

⁝

![ff-bonus---merge-ff](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/677kl3e9pm8zoei1xah3.png)

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:

> [Merge your commits](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/incorporating-changes-from-a-pull-request/about-pull-request-merges#merge-your-commits)
>
> 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:

![merge---state-to-reproduce](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bk70q7h8zx95brpb0bu2.png)

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`

![merge---after-merge](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cfy30gr5yvtc2ipipbea.png)

There are two things to notice:

1. 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 of `git pull` and `git merge`.
2. Default messages generated by GitHub and by Git differ.
   1. 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.
   2. I described default messages from GitHub in the [first post of this series](https://dev.to/zdybit/3-options-to-close-pull-requests-on-github-what-2j3n).

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:

> [Squash and merge your commits](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/incorporating-changes-from-a-pull-request/about-pull-request-merges#squash-and-merge-your-commits)
>
> 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](https://git-scm.com/docs/git-merge#Documentation/git-merge.txt---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:

![squash---state-to-reproduce](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/79ksy0afi6y4h2r7339y.png)

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.

![squash---after-squash](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sof2ouzwqp4wtext97gq.png)

`$ git commit`

![squash---vscode-commit-editor](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ks7d04u558txhw0feqzu.png)

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`

![squash---after-squash-n-push](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lcp24zh7ucdy1zkpw2cv.png)

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:

> [Rebase and merge your commits](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/incorporating-changes-from-a-pull-request/about-pull-request-merges#rebase-and-merge-your-commits)
>
> 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:

![rebase---state-to-reproduce](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nsnlbnjqo9ccveeze9m5.png)

This time, it is not enough to run one or two Git commands to achieve the same result.

Long story short, we need to:

1. rebase the `feature/9` branch onto the `main`
2. move the `main` pointer to `feature/9`
3. reset `feature/9` to `origin/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`

![rebase---after-rebase-on-local](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/le6hl00urn9683xlg9tl.png)

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`

![rebase---after-rebase-n-merge](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6ee8mg3pk67tfqehipwa.png)

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.

![rebase---after-rebase-n-reset](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/18n3g7q7duxxp24wks77.png)

**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.