Getting Geeky with Git

Fast-forward merge and merge strategies

Marcin Wanago
Git

When working with branches, we often need to synchronize our changes. When doing so, we can implement different approaches. In this article, we explain how merging works and discuss various situations. During that, we will touch on the subject of the fast-forward merging and different merge strategies.

The basics of merging

The job of the  git merge command is to integrate a code history that we’ve forked at some point. Let’s look deeper into how it works. First, let’s create a new branch and make some changes.

1git checkout -b new-branch
2echo "Additional line of code" >> README.md
3git add ./README.md 
4git commit -m "Added a line of code"

We see the current state of the branch with  git log:

commit b53e0718f93eff963181b8c6ef92341641141641 (HEAD -> new-branch) Author: Marcin Wanago <wanago.marcin@gmail.com> Date: Sat Jul 18 18:03:06 2020 +0200 Added a line of code commit cf418d2c640d839570fe3151fc3f12116c118db9 (origin/master, master) Author: Marcin Wanago <wanago.marcin@gmail.com> Date: Sat Jul 18 17:52:01 2020 +0200 initial commit

Now, let’s move back to master and merge the changes:

1git checkout master
2git merge new-branch
Updating cf418d2..b53e071 Fast-forward README.md | 1 + 1 file changed, 1 insertion(+)

Fast Forward Merge

One of the most important things about  git merge, when compared to  git rebase, is that merging creates a merge commit. This is not always the case, though. Let’s look into the  git log after the above commit:

commit b53e0718f93eff963181b8c6ef92341641141641 (HEAD -> master, new-branch) Author: Marcin Wanago <wanago.marcin@gmail.com> Date: Sat Jul 18 18:03:06 2020 +0200 Added a line of code commit cf418d2c640d839570fe3151fc3f12116c118db9 (origin/master) Author: Marcin Wanago <wanago.marcin@gmail.com> Date: Sat Jul 18 17:52:01 2020 +0200 initial commit

Since our  new-branch is very simple, a fast forward merge occurred. It works by combining the histories of both branches. It can happen when there is a linear path from the current branch tip to the target branch. The above is the case since we haven’t committed anything to master before creating the  new-branch.

In the third part of this series, we can learn that the branch is a reference.

If we dig a bit deeper, we can see that both  master and the  new-branch now point to the same commit. This is the case thanks to performing a fast-forward merge.

1git show-branch --sha1-name master
[b53e071] Added a line of code
1git show-branch --sha1-name new-branch
[b53e071] Added a line of code

True merge

However, the above is not possible if our branches have diverged. To create such an example, let’s create a new branch but then make some changes to  master.

1git checkout -b feature-b
2git checkout master
3echo "console.log('Feature A')" >> feature-a.js
4git add ./feature-a.js
5git commit -m "Added feature A"

Now that our master includes some new changes let’s go back to feature-b.

1git checkout feature-b
2echo "console.log('Feature B')" >> feature-b.js
3git add ./feature-b.js
4git commit -m "Added feature B"

Now, let’s merge  feature-b to  master.

1git checkout master
2git merge feature-b

Once we do the above, Git fires up the text editor. The default depends on your system. In my case, it is Nano:

The editor opens because the merge results in creating a merge commit. Once we finalize the merge, we get the following:

Merge made by the ‘recursive’ strategy. feature-b.js | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 feature-b.js

In the above graph, we can observe at which point the  master diverges into  feature-b and when it comes together with the use of the merge commit.

Let’s look into the merge commit a bit more to understand it better.

1git cat-file -p 73575db27f3c8a9d7492139013eae0e01193b880
tree 6b45e53ad2e6946d3a9f38ccf33a93ff8ef28a28 parent ff435740aad2a498294bd162e611f9a876c2b489 parent 86251d7ea21d1b718e249ca83ae93dbdbb480e48 author Marcin Wanago <wanago.marcin@gmail.com> 1595166982 +0200 committer Marcin Wanago <wanago.marcin@gmail.com> 1595166982 +0200 Merge branch ‘feature-b’

In the second part of this series, we’ve learned that a parent of a commit is simply the previous commit. When we create a merge commit, it has multiple parents. Let’s inspect them:

1git cat-file -p ff435740aad2a498294bd162e611f9a876c2b489
// … Added feature A
1git cat-file -p 86251d7ea21d1b718e249ca83ae93dbdbb480e48
// … Added feature B

We can see that the parents of the merge commit are the tips of the branches involved.

Merge strategies

When we attempt to merge two branches, Git tries to find a common base commit. When looking for the latest common commit between two branches, Git can adopt one of a few strategies.

Resolve

It works for merging two branches, and it used to be the default. A detailed explanation of this strategy can be found in Version Control with Git

[…] pick one of the possible merge bases […] and hope for the best. […] Git detects that it’s remerging some changes that are already in place and just skips duplicate changes, avoiding the conflict. Or, if there are slight changes that do cause a conflict, at least the conflicts should be fairly easy for a developer to handle.

Octopus

It is a default strategy when attempting to merge more than two branches. It fails to do so if it encounters situations in which a manual resolution is required. A merge commit created with this strategy has more than two parents.

Recursive

The recursive strategy became the default when pulling or merging two branches in 2005. It has proven to cause fewer conflicts when working on the Linux kernel as opposed to the resolve strategy.

It uses a three-way merge algorithm to recurse over the changes in the branches. The recursive strategy has quite a few options available, such as ours and theirs. For a full list, check out the documentation.

Subtree

It is a form of a recursive strategy. It might prove to be useful when managing multiple projects within a single repository. Github has quite a detailed documentation on this topic with examples.

Ours

When merging, it discards changes from the other branch (or multiple branches), merging just the history. It does not affect the files at all. According to the documentation, it is meant to be used to supersede the old development history of side branches. It differs from using the recursive strategy with the “ours” flag.

Summary

Although it is usually the best idea to rely on Git to choose the best approach to merging, it is useful to have at least a basic understanding of the reasons behind it. When dealing with merges, we might alter our approach slightly to have a cleaner history and fewer conflicts. One of the additional processes that we might want to introduce to our flow is rebasing, and we will cover it in the upcoming parts of this series.