- 1. Getting geeky with Git #1. Remotes and upstream branches
- 2. Getting geeky with Git #2. Building blocks of a commit
- 3. Getting geeky with Git #3. The branch is a reference
- 4. Getting geeky with Git #4. Fast-forward merge and merge strategies
- 5. Getting geeky with Git #5. Improving merge workflow with rebase
- 6. Getting geeky with Git #6. Interactive Rebase
- 7. Getting geeky with Git #7. Cherry Pick with Reflog
- 8. Getting geeky with Git #8. Improving our debugging flow with Bisect and Worktree
- 9. Getting geeky with Git #9. Understanding the revert feature
- 10. Getting geeky with Git #10. The overview of Git hooks with Husky
- 11. Getting geeky with Git #11. Keeping our Git history clean with fixup commits
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.
1 2 3 4 |
git checkout -b new-branch echo "Additional line of code" >> README.md git add ./README.md git 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 +0200Added a line of code
commit cf418d2c640d839570fe3151fc3f12116c118db9 (origin/master, master)
Author: Marcin Wanago <wanago.marcin@gmail.com>
Date: Sat Jul 18 17:52:01 2020 +0200initial commit
Now, let’s move back to master and merge the changes:
1 2 |
git checkout master git 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 +0200Added a line of code
commit cf418d2c640d839570fe3151fc3f12116c118db9 (origin/master)
Author: Marcin Wanago <wanago.marcin@gmail.com>
Date: Sat Jul 18 17:52:01 2020 +0200initial 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.
1 |
git show-branch --sha1-name master |
[b53e071] Added a line of code
1 |
git 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.
1 2 3 4 5 |
git checkout -b feature-b git checkout master echo "console.log('Feature A')" >> feature-a.js git add ./feature-a.js git commit -m "Added feature A" |
Now that our master includes some new changes let’s go back to feature-b.
1 2 3 4 |
git checkout feature-b echo "console.log('Feature B')" >> feature-b.js git add ./feature-b.js git commit -m "Added feature B" |
Now, let’s merge feature-b to master.
1 2 |
git checkout master git 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.
1 |
git 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 +0200Merge 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:
1 |
git cat-file -p ff435740aad2a498294bd162e611f9a876c2b489 |
// …
Added feature A
1 |
git 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.