
How to Resolve Merge Conflicts
How to Resolve Merge Conflicts κ΄λ ¨
By following so far, you should understand the basics of git merge
, and how Git can automatically resolve some conflicts. You also understand what cases are automatically resolved.
Next, let's consider a more advanced case.
Say Paul and John keep working on this song.
Paul creates a new branch:
git checkout -b paul_branch_4
And he decides to add some "Yeah"s to the song, so he changes this verse as follows:

<Source: Brief>
So Paul stages and commits these changes:
git add everyone.md
git commit -m "Commit 13"
Paul also creates another song, let_it_be.md
and adds it to the repo:
git add let_it_be.md git commit -m "Commit 14"
This is the history:

<Source: Brief>
Back to main
:
git checkout main
John also branches out:
git checkout -b john_branch_4
And John also works on the song "Everyone had a hard year", later to be called "I've got a feeling" (again, this is not an article about the Beatles, so I won't elaborate on it here. See the appendix if you are curious).
John decides to change all occurrences of "Everyone" to "Everybody":

<Source: Brief>
He stages and commits this song to the repo:
git add everyone.md
git commit -m "Commit 15"
Nice. Now John also creates another song, across_the_universe.md
. He adds it to the repo as well:
git add across_the_universe.md
git commit -m "Commit 16"
Observe the history again:

<Source: Brief>
You can see that the history diverges from main
, to two different branches β paul_branch_4
, and john_branch_4
.
At this point, John would like to merge the changes introduced by Paul.
What is going to happen here?
Remember the changes introduced by Paul:
git diff main paul_branch_4

git diff main paul_branch_4
<Source: Brief>
What do you think? Will merge work? π€
Try it out:
git merge paul_branch_4

<Source: Brief>
We have a conflict! π₯
It seems that Git cannot merge these branches on its own. You can get an overview of the merge state, using git status
:

git status
right after the merge
operation<Source: Brief>
The changes that Git had no problem resolving are staged for commit. And there is a separate section for "unmerged paths" β these are files with conflicts that Git could not resolve on its own.
It's time to understand why and when these conflicts happen, how to resolve them, and also how Git handles them under the hood. Alright then! I hope you are at least as excited as I am. π
Let's recall what we know about 3-way merges:
First, Git will look for the merge base β the common ancestor of john_branch_4
and paul_branch_4
. Which commit would that be?
Correct, it would be the tip of main
branch, the commit in which we merged john_branch_3
into paul_branch_3
.
Again, if you are not sure, you can verify that by running:
git merge-base john_branch_4 paul_branch_4
And at the current state, git status
knows which files are staged and which aren't.
Consider the process for each file, which is the same as the 3-way merge algorithm we considered per line, but on a file's level:
across_the_universe.md
exists on John's branch, but doesn't exist on the merge base or on Paul's branch. So Git chooses to include this file. Since you are already on John's branch and this file is included in the tip of this branch, it is not mentioned by git status
.let_it_be.md
exists on Paul's branch, but doesn't exist on the merge-base or John's branch. So git merge
"chooses" to include it.What about everyone.md
? Well, here we have three different states of this file: its state on the merge base, its state on John's branch, and its state on Paul's branch. While performing a merge
, Git stores all of these versions on the index.
Let's observe that by looking directly at the index with the command git ls-files
:
git ls-files -s β-abbrev

git ls-files -s β-abbrev
after the merge operation<Source: Brief>
You can see that everyone.md
has three different entries. Git assigns each version a number that represents the "stage" of the file, and this is a distinct property of an index entry, alongside the file's name and the mode bits (I covered the index in a previous post (swimm
)).
When there is no merge conflict regarding a file, its "stage" is 0
. This is indeed the state for across_the_universe.md
, and for let_it_be.md
.
On a conflict's state, we have:
- Stage
1
β which is the merge base. - Stage
2
β which is "your" version. That is, the version of the file on the branch you are merging into. In our example, this would bejohn_branch_4
. - Stage
3
β which is "their" version, also called theMERGE_HEAD
. That is, the version on the branch you are merging (into the current branch). In our example, that ispaul_branch_4
.
To observe the file's contents in a specific stage, you can use a command I introduced in a previous post (swimm
), git cat-file
, and provide the blob's SHA:
git cat-file -p

git cat-file
to present the content of the file on John's branch, right from its state in the index<Source: Brief>
And indeed, this is the content we expected β from John's branch, where the lines start with "Everybody" rather than "Everyone".
A nice trick that allows you to see the content quickly without providing the blob's SHA-1 value, is by using git show
, like so:
git show ::everyone.md
For example, to get the content of the same version as with git cat-file -p <BLOB_SHA_FOR_STAGE_2>
, you can write git show :2:everyone.md
.
Git records the three states of the three commits into the index in this way at the start of the merge. It then follows the three-way merge algorithm to quickly resolve the simple cases:
In case all three stages match, then the selection is trivial.
If one side made a change while the other did nothing β that is, stage 1 matches stage 2, then we choose stage 3 β or vice versa. That's exactly what happened with let_it_be.md
and across_the_universe.md
.
In case of a deletion on the incoming branch, for example, and given there were no changes on the current branch, then we would see that stage 1 matches stage 2, but there is no stage 3. In this case, git merge
removes the file for the merged version.
What's really cool here is that for matching, Git doesn't need the actual files. Rather, it can rely on the SHA-1 values of the corresponding blobs. This way, Git can easily detect the state a file is in.

<Source: Brief>
Cool, so for everyone.md
you have this special case β where stage 1, stage 2 and stage 3 are all different from one another. That is, they have different blob SHAs. It's time to go deeper and understand the merge conflict. π
One way to do that would be to simply use git diff
. In a previous post, we examined git diff
in detail, and saw that it shows the differences between various combinations of the working tree, index or commits.
But git diff
also has a special mode for helping with merge conflicts:
git diff

git diff
during a conflict<Source: Brief>
This output may be confusing at first, but once you get used to it, it's pretty clear. Let's start by understanding it, and then see how you can resolve conflicts with other, more visual tools.
The conflicted section is separated by the "equal" marks (====), and marked with the corresponding branches. In this context, "ours" is the current branch. In this example, that would be john_branch_4
, the branch that HEAD
was pointing to when we initiated the git merge
command. "Theirs" is the MERGE_HEAD
, the branch that we are merging in β in this case, paul_branch_4
.
So git diff
without any special flags shows changes between the working tree and the index, which in this case are the conflicts yet to be resolved. The output doesn't include staged changes, which is very convenient for resolving the conflict.
Time to resolve this manually. Fun!
So, why is this a conflict?
For Git, Paul and John made different changes to the same line, for a few lines. John changed it to one thing, and Paul changed it to another thing. Git cannot decide which one is correct.
This is not the case for the last lines, like the line that used to be "Everyone had a hard year" on the merge base. Paul hasn't changed this line, or the lines surrounding it, so its version on paul_branch_4
, or "theirs" in our case, agrees with the merge_base. Yet John's version, "ours", is different. Thus git merge
can easily decide to take this version.
But what about the conflicted lines?
In this case, I know what I want, and that is actually a combination of these lines. I want the lines to start with Everybody
, following John's change, but also to include Paul's "yeah"s. So go ahead and create the desired version by editing everyone.md
: nano everyone.md

<Source: Brief>
To compare the result file to what you had in the branch prior to the merge, you can run:
git diff --ours
Similarly, if you wish to see how the result of the merge differs from the branch you merged into our branch, you can run:
git diff -βtheirs
You can even see how the result is different from both sides using:
git diff -βbase
Now you can stage the fixed version:
git add everyone.md
After staging, if you look at git status
, you will see no conflicts:

everyone.md
, there are no conflicts<Source: Brief>
You can now simply use git commit
, and Git will present you with a commit message containing details about the merge. You can modify it if you like, or leave it as is. Regardless of the commit message, Git will create a "merge commit" β that is, a commit with more than one parent.
To validate that, consider the history:

<Source: Brief>
john_branch_4
now points to the new merge commit. The incoming branch, "theirs", in this case, paul_branch_4
, stays where it was.