
By reading this post, you are going to really understand git merge
, one of the most common operations you’ll perform in your Git repositories.
Notes before we start
- I also created two videos covering the contents of this post. If you wish to watch alongside reading, you can find them here (Part 1, Part 2).
- I am working on a book about Git! Are you interested in reading the initial versions and providing feedback? Send me an email: gitting.things@gmail.com
OK, are you ready?
- What is a Merge in Git?
- Time to Get Hands-on 🙌🏻
- Time For a More Advanced Case
- Quick recap on a three-way merge
- Moving on 👣
- More Advanced Git Merge Cases
- How Git’s 3-way Merge Algorithm Works
- How to Resolve Merge Conflicts
- How to Use VS Code to Resolve Conflicts
- One More Powerful Tool 🪛
- Recap
Merging is the process of combining the recent changes from several branches into a single new commit that will be on all those branches.
In a way, merging is the complement of branching in version control: a branch allows you to work simultaneously with others on a particular set of files, whereas a merge allows you to later combine separate work on branches that diverged from a common ancestor commit.
OK, let’s take this bit by bit.
Remember that in Git, a branch is just a name pointing to a single commit. When we think about commits as being “on” a specific branch, they are actually reachable through the parent chain from the commit that the branch is pointing to.
That is, if you consider this commit graph:

You see the branch feature_1
, which points to a commit with the SHA-1 value of ba0d2
. Of course, as in other posts, I only write the first 5 digits of the SHA-1 value.
Notice that commit 54a9d
is also on this branch, as it is the parent commit of ba0d2
. So if you start from the pointer of feature_1
, you get to ba0d2
, which then points to 54a9d
.
When you merge with Git, you merge commits. Almost always, we merge two commits by referring to them with the branch names that point to them. Thus we say we “merge branches” – though under the hood, we actually merge commits.
OK, so let’s say I have this simple repository here, with a branch called main
, and a few commits with the commit messages of “Commit 1”, “Commit 2” and “Commit 3”:

Next, create a feature branch by typing git branch new_feature
:

git branch
(Source: Brief)And switch HEAD
to point to this new branch, by using git checkout new_feature
. You can look at the outcome by using git log
:

git log
after using git checkout new_feature
(Source: Brief)As a reminder, you could also write git checkout -b new_feature
, which would both create a new branch and change HEAD
to point to this new branch.
If you need a reminder about branches and how they’re implemented under the hood, please check out a previous post on the subject. Yes, check out. Pun intended 😇
Now, on the new_feature
branch, implement a new feature. In this example I will edit an existing file that looks like this before the edit:

code.py
before editing it (Source: Brief)And I will now edit it to include a new function:

new_feature
(Source: Brief)And thankfully, this is not a programming tutorial, so this function is legit 😇
Next, stage and commit this change:

Looking at the history, you have the branch new_feature
, now pointing to “Commit 4”, which points to its parent, “Commit 3”. The branch main
is also pointing to “Commit 3”.
Time to merge the new feature! That is, merge these two branches, main
and new_feature
. Or, in Git’s lingo, merge new_feature
into main
. This means merging “Commit 4” and “Commit 3”. This is pretty trivial, as after all, “Commit 3” is an ancestor of “Commit 4”.
Check out the main branch (with git checkout main
), and perform the merge by using git merge new_feature
:

new_feature
into main
(Source: Brief)Since new_feature
never really diverged from main
, Git could just perform a fast-forward merge. So what happened here? Consider the history:

Even though you used git merge
, there was no actual merging here. Actually, Git did something very simple – it reset the main
branch to point to the same commit as the branch new_feature
.
In case you don’t want that to happen, but rather you want Git to really perform a merge, you could either change Git’s configuration, or run the merge
command with the --no-ff
flag.
First, undo the last commit:
git reset --hard HEAD~1
If this way of using reset is not clear to you, feel free to check out a post where I covered git reset
in depth. It is not crucial for this introduction of merge
, though. For now, it’s important to understand that it basically undoes the merge operation.
Just to clarify, now if you checked out new_feature
again:
git checkout new_feature
The history would look just like before the merge:

git reset --hard HEAD~1
(Source: Brief)Next, perform the merge with the --no-fast-forward
flag (--no-ff for short
):
git checkout main
git merge new_feature --no-ff
Now, if we look at the history using git lol
:

--no-ff
flag (Source: Brief)(git lol
is an alias I added to Git to visibly see the history in a graphical manner. You can find it here).
Considering this history, you can see Git created a new commit, a merge commit.
If you consider this commit a bit closer:
git log -n1

You will see that this commit actually has two parents – “Commit 4”, which was the commit that new_feature
pointed to when you ran git merge
, and “Commit 3”, which was the commit that main
pointed to. So a merge commit has two parents: the two commits it merged.
The merge commit shows us the concept of merge quite well. Git takes two commits, usually referenced by two different branches, and merges them together.
After the merge, as you started the process from main
, you are still on main
, and the history from new_feature
has been merged into this branch. Since you started with main
, then “Commit 3”, which main
pointed to, is the first parent of the merge commit, whereas “Commit 4”, which you merged into main
, is the second parent of the merge commit.
Notice that you started on main
when it pointed to “Commit 3”, and Git went quite a long way for you. It changed the working tree, the index, and also HEAD
and created a new commit object. At least when you use git merge
without the --no-commit
flag and when it’s not a fast-forward merge, Git does all of that.
This was a super simple case, where the branches you merged didn’t diverge at all.
By the way, you can use git merge
to merge more than two commits – actually, any number of commits. This is rarely done and I don’t see a good reason to elaborate on it here.
Another way to think of git merge
is by joining two or more development histories together. That is, when you merge, you incorporate changes from the named commits, since the time their histories diverged from the current branch, into the current branch. I used the term branch
here, but I am stressing this again – we are actually merging commits.
Time to consider a more advanced case, which is probably the most common case where we use git merge
explicitly – where you need to merge branches that did diverge from one another.
Assume we have two people working on this repo now, John and Paul.
John created a branch:
git checkout -b john_branch

john_branch
(Source: Brief)And John has written a new song in a new file, lucy_in_the_sky_with_diamonds.md
. Well, I believe John Lennon didn’t really write in Markdown format, or use Git for that matter, but let’s pretend he did for this explanation.
git add lucy_in_the_sky_with_diamonds.md
git commit -m "Commit 5"
While John was working on this song, Paul was also writing, on another branch. Paul had started from main
:
git checkout main
And created his own branch:
git checkout -b paul_branch
And Paul wrote his song into a file:
nano penny_lane.md
And committed it:
git add penny_lane.md
git commit -m "Commit 6"
So now our history looks like this – where we have two different branches, branching out from main
, with different histories.

git lol
shows the history after John and Paul committed (Source: Brief)John is happy with his branch (that is, his song), so he decides to merge it into the main
branch:
git checkout main
git merge john_branch
Actually, this is a fast-forward merge, as we have learned before. You can validate that by looking at the history (using git lol
, for example):

john_branch
into main
results in a fast-forwrad merge (Source: Brief)At this point, Paul also wants to merge his branch into main
, but now a fast-forward merge is no longer relevant – there are two different histories here: the history of main
‘s and that of paul_branch
‘s. It’s not that paul_branch
only adds commits on top of main
branch or vice versa.
Now things get interesting. 😎😎
First, let Git do the hard work for you. After that, we will understand what’s actually happening under the hood.
git merge paul_branch
Consider the history now:

paul_branch
, you get a new merge commit (Source: Brief)What you have is a new commit, with two parents – “Commit 5” and “Commit 6”.
In the working dir, you can see that both John’s song as well as Paul’s song are there:
ls

Nice, Git really did merge the changes for us. But how does that happen?
Undo this last commit:
git reset --hard HEAD~
How to perform a three-way merge in Git
It’s time to understand what’s really happening under the hood. 😎
What Git has done here is it called a 3-way merge
. In outlining the process of a 3-way merge, I will use the term “branch” for simplicity, but you should remember you could also merge two (or more) commits that are not referenced by a branch.
The 3-way merge process includes these stages:
First, Git locates the common ancestor of the two branches. That is, the common commit from which the merging branches most recently diverged. Technically, this is actually the first commit that is reachable from both branches. This commit is then called the merge base.
Second, Git calculates two diffs – one diff from the merge base to the first branch, and another diff from the merge base to the second branch. Git generates patches based on those diffs.
Third, Git applies both patches to the merge base using a 3-way merge algorithm. The result is the state of the new, merge commit.

So, back to our example.
In the first step, Git looks from both branches – main
and paul_branch
– and traverses the history to find the first commit that is reachable from both. In this case, this would be…which commit?
Correct, “Commit 4”.
If you are not sure, you can always ask Git directly:
git merge-base main paul_branch
By the way, this is the most common and simple case, where we have a single obvious choice for the merge base. In more complicated cases, there may be multiple possibilities for a merge base, but this is a topic for another post.
In the second step, Git calculates the diffs. So it first calculates the diff between “Commit 4” and “Commit 5”:
git diff 4f90a62 4683aef
(The SHA-1 values will be different on your machine)

If you don’t feel comfortable with the output of git diff
, please read the previous post where I described it in detail.
You can store that diff to a file:
git diff 4f90a62 4683aef > john_branch_diff.patch
Next, Git calculates the diff between “Commit 4” and “Commit 6”:
git diff 4f90a62 c5e4951

Write this one to a file as well:
git diff 4f90a62 c5e4951 > paul_branch_diff.patch
Now Git applies those patches on the merge base.
First, try that out directly – just apply the patches (I will walk you through it in a moment). This is not what Git really does under the hood, but it will help you gain a better understanding of why Git needs to do something different.
Checkout the merge base first, that is, “Commit 4”:
git checkout 4f90a62
And apply John’s patch first:
git apply -–index john_branch_diff.patch
Notice that for now there is no merge commit. git apply
updates the working dir as well as the index, as we used the --index
switch.
You can observe the status using git status
:

So now John’s new song is incorporated into the index. Apply the other patch:
git apply -–index paul_branch_diff.patch
As a result, the index contains changes from both branches.
Now it’s time to commit your merge. Since the porcelain command git commit
always generates a commit with a single parent, you would need the underlying plumbing command – git commit-tree
.
If you need a reminder about porcelain vs plumbing commands, check out the post where I explained these terms, and created an entire repo from scratch.
Remember that every Git commit object points to a single tree. So you need to record the contents of the index in a tree:
git write-tree
Now you get the SHA-1 value of the created tree, and you can create a commit object using git commit-tree
:
git commit-tree -p -p -m "Merge commit!"

Great, so you have created a commit object 💪🏻
Recall that git merge
also changes HEAD
to point to the new merge commit object. So you can simply do the same:
git reset –-hard db315a
If you look at the history now:

HEAD
(Source: Brief)You can see that you’ve reached the same result as the merge done by Git, with the exception of the timestamp and thus the SHA-1 value, of course.
So you got to merge both the contents of the two commits – that is, the state of the files, and also the history of those commits – by creating a merge commit that points to both histories.
In this simple case, you could actually just apply the patches using git apply
, and everything worked quite well.
Quick recap on a three-way merge
So to quickly recap, on a three-way merge, Git:
- First, locates the merge base – the common ancestor of the two branches. That is, the first commit that is reachable from both branches.
- Second, Git calculates two diffs – one diff from the merge base to the first branch, and another diff from the merge base to the second branch.
- Third, Git applies both patches to the merge base, using a 3-way merge algorithm. I haven’t explained the 3-way merge yet, but I will elaborate on that later. The result is the state of the new, merge commit.
You can also understand why it’s called a “3-way merge”: Git merges three different states – that of the first branch, that of the second branch, and their common ancestor. In our previous example, main
, paul_branch
, and Commit 4
.
This is unlike, say, the fast-forward examples we saw before. The fast-forward examples are actually a case of a two-way merge, as Git only compares two states – for example, where main
pointed to, and where john_branch
pointed to.
Still, this was a simple case of a 3-way merge. John and Paul created different songs, so each of them touched a different file. It was pretty straightforward to execute the merge.
What about more interesting cases?
Let’s assume that now John and Paul are co-authoring a new song.
So, John checkedout main
branch and started writing the song:
git checkout main

He staged and committed it (“Commit 7”):

Now, Paul branches:
git checkout -b paul_branch_2
And edits the song, adding another verse:

Of course, in the original song, we don’t have the title “Paul’s Verse”, but I’ll add it here for simplicity.
Paul stages and commits the changes:
git add a_day_in_the_life.md
git commit -m "Commit 8"
John also branches out from main
and adds a few last lines:
git checkout -b john_branch_2


And he stages and commits his changes too (“Commit 9”):

This is the resulting history:

So, both Paul and John modified the same file on different branches. Will Git be successful in merging them? 🤔
Say now we don’t go through main,
but John will try to merge Paul’s new branch into his branch:
git merge paul_branch_2
Wait!! 🤚🏻 Don’t run this command! Why would you let Git do all the hard work? You are trying to understand the process here.
So, first, Git needs to find the merge base. Can you see which commit that would be?
Correct, it would be the last commit on main
branch, where the two diverged.
You can verify that by using:
git merge-base john_branch_2 paul_branch_2

Great, now Git should compute the diffs and generate the patches. You can observe the diffs directly:
git diff main paul_branch_2

git diff main paul_branch_2
(Source: Brief)Will applying this patch succeed? Well, no problem, Git has all the context lines in place.
Ask Git to apply this patch:
git diff main paul_branch_2 > paul_branch_2.patch
git apply -–index paul_branch_2.patch
And this worked, no problem at all.
Now, compute the diff between John’s new branch and the merge base. Notice that you haven’t committed the applied changes, so john_branch_2
still points at the same commit as before, “Commit 9”:
git diff main john_branch_2

git diff main john_branch_2
(Source: Brief)Will applying this diff work?
Well, indeed, yes. Notice that even though the line numbers have changed on the current version of the file, thanks to the context lines Git is able to locate where it needs to add these lines…

Save this patch and apply it then:
git diff main john_branch_2 > john_branch_2.patch
git apply –-index john_branch_2.patch

Observe the result file:

Cool, exactly what we wanted 👏🏻
You can now create the tree and relevant commit:
git write-tree
Don’t forget to specify both parents:
git commit-tree -p paul_branch_2 -p john_branch_2 -m "Merging new changes"

See how I used the branches names here? After all, they are just pointers to the commits we want.
Cool, look at the log from the new commit:

Exactly what we wanted.
You can also let Git perform the job for you. You can simply checkout john_branch_2
, which you haven’t moved – so it still points to the same commit as it did before the merge. So all you need to do is run:
git merge paul_branch_2
Observe the resulting history:

Just as before, you have a merge commit pointing to “Commit 8” and “Commit 9” as its parents. “Commit 9” is the first parent since you merged into it.
But this was still quite simple… John and Paul worked on the same file, but on very different parts. You could also directly apply Paul’s changes to John’s branch. If you go back to John’s branch before the merge:
git reset --hard HEAD~
And now apply Paul’s changes:
git apply -–index paul_branch_2.patch

You will get the same result.
But what happens when the two branches include changes on the same files, in the same locations? 🤔
What would happen if John and Paul were to coordinate a new song, and work on it together?
In this case, John creates the first version of this song in the main
branch:
git checkout main
nano everyone.md

everyone.md
prior to the first commit (Source: Brief)By the way, this text is indeed taken from the version that John Lennon recorded for a demo in 1968. But this isn’t an article about the Beatles, so if you’re curious about the process the Beatles underwent while writing this song, you can follow the links in the appendix below.
git add everyone.md
git commit -m "Commit 10"

Now John and Paul split. Paul creates a new verse in the beginning:
git checkout -b paul_branch_3
nano everyone.md

Also, while talking to John, they decided to change the word “feet” to “foot”, so Paul adds this change as well.
And Paul adds and commits his changes to the repo:
git add everyone.md
git commit -m "Commit 11"
You can observe Paul’s changes, by comparing this branch’s state to the state of br