How does Git decide when there's a merge conflict?
October 18, 2025
After working through a merge conflict, Sam asked me "how does Git decide when there's a merge conflict versus just automerging anyway?" I was annoyed that I couldn't give a simple answer, so I looked it up and here it is:
The basics
When you merge two branches, Git performs a three-way merge. This just means it constructs a new merge-specific working tree by walking three trees in parallel: branch A, branch B, and their common ancestor (the base). Whenever it reaches a file where the blob hashes differ between any of the three trees, it applies this rule of thumb:
For every line in the base...
- If exclusively A or B changes the line, keep that side's change (no conflict).
- If both A and B change the line, declare a merge conflict.
Here's an example where we keep A's change (left) versus where Git declares a conflict (right):
# Base
print("hello world")
print("A")
print("B")
print("C")
# A
print("hello world")
print("DOG")
print("B")
print("C")
# B
print("hello world")
print("A")
print("B")
print("C")
# Base
print("hello world")
print("A")
print("B")
print("C")
# A
print("hello world")
print("DOG")
print("B")
print("C")
# B
print("hello world")
print("CAT")
print("B")
print("C")
And that's it! 🎉 Pretty simple.
But wait, what about adding new lines? Deleting lines? Moving lines?Okay, I've been slightly lying to you so far. The actual thing that git checks isn't whether two edits are made to the same line, but whether the patches A and B have any overlapping hunks.
Hunks
Now we need to introduce the concept of hunks. Hunks actually come from diff algorithms, not Git itself. By default Git uses Myers' algorithm.1
See here for a great explanation of what a hunk is. A hunk is a contiguous group of changed lines in a diff, optionally surrounded by some context lines for human readability.
We can write the diff between any two files as:
- Diff = Blocks where the two files are the same + Blocks where the two files differ
print("A")
print("DOG")
print("B")
print("RED")
print("D")
print("A")
print("CAT")
print("B")
print("BLUE")
print("D")
Same: print("A")
Hunk 1: { print("DOG") ↔ print("CAT") }
Same: print("B")
Hunk 2: { print("RED") ↔ print("BLUE") }
Same: print("D")
Hunks depend on diff algorithm
I said earlier that hunks come from diff algorithms. Here's something surprising: two different diff algorithms can yield two different sets of hunks for a given set of files.
Suppose the file F contains the three lines 'a', 'b', 'c', and the file G contains the same three lines in reverse order: 'c', 'b', 'a'. If diff finds the line 'c' as common, then the command 'diff F G' produces this output:2
1,2d0
< a
< b
3a2,3
> b
> a
But if diff notices the common line 'b' instead, it produces this output:
1c1
< a
---
> c
3c3
< c
---
> a
There are many different ways for a diff algorithm to decide on which hunks to use to represent the diff between two files, and it's up to the diff algorithm to make hunks minimal and readable to make life easier for the reviewer.3
Hunks in Git's merge algorithm
Okay, great. Now that we understand the concept of hunks, we can give the real way the Git decides when there's a merge conflict:
- It applies changes
base → Aandbase → B. - It checks to see if the patches
base → Aandbase → Bhave any overlapping hunks. If there are, that's a merge conflict.
Here's an example:
# Base
print("hello world")
print("A")
print("B")
print("C")
# Commit A
print("hello world")
print("DOG")
print("B")
print("C")
diff A (ie `git diff base A`):
@@ -1,4 +1,5 @@
print("hello world")
print("A")
+print("DOG")
print("B")
print("C")
\ No newline at end of file
# Base
print("hello world")
print("A")
print("B")
print("C")
# Commit B
print("hello world")
print("CAT")
print("B")
print("C")
diff B (ie `git diff base B`):
@@ -1,4 +1,5 @@
print("hello world")
print("A")
+print("CAT")
print("B")
print("C")
\ No newline at end of file
Here the hunks overlap, so Git reports a conflict.
Wait, but what does it mean for hunks to overlap?
Each hunk's length can be measured by (old_start, old_length, new_start, new_length). That's what notation like @@ -1,4 +1,5 @@ is that you see in git diff! (Here, we see that the lengths are 4 and 5 rather than 0 and 1 because git includes 2 lines of context on either side for human readability)
So, hunks A and B overlap iff the ranges (a.old_start, a.old_length) (b.old_start, b.old_length) intersect. Why use old rather than new? Because the base file is a convenient common reference point between A and B. This is one of the fundamental reasons why using a 3-way merge is helpful.
Aside: contextual lines
Okay, I was lying to you again. Remember when I said "hunks A and B overlap if the ranges (a.old_start, a.old_length) (b.old_start, b.old_length) intersect"? This is not quite true. The window we see when we run git diff includes 2 lines of context on either side for human readability.4 The context lines are not considered as part of the hunks for the purposes of determining merge conflicts.
So, consider these diffs:
@@ -1,4 +1,5 @@
print("hello world")
print("A")
+print("DOG")
print("B")
print("C")
\ No newline at end of file
@@ -1,4 +1,5 @@
print("hello world")
print("A")
print("B")
+print("CAT")
print("C")
\ No newline at end of file
And this base:
print("hello world")
print("A")
print("B")
print("C")
We can make this merge with no conflicts, since the Git-readable hunks do not intersect, although when you include the surrounding context that Git adds for human readability, it appears that they do! DOG clearly appears between A and B whereas CAT appears between B and C.
Conclusion
So the real procedure is this:
- It applies changes base→A and base→B
- It checks to see if the patches base→A and base→B have any overlapping hunks. If they do, then those lines are marked as a merge conflict and manually added to the merge tree.
And that's it! 🎉 This is how Git can tell if there is a merge conflict. Except for a number of additional cases:
- File rename conflicts
- Deleting + editing a file
- Binary file conflicts
- Directory / file name conflicts
- Whitespace-only conlicts
- ...and many more
These are less interesting so I won't cover them.
- If you're a nerd you can manually choose which diff algorithm you want Git to use in .gitattributes. ↩
- This example is taken from the GNU diffutils docs. ↩
- This has implications for codebase analytics too- your PR that GitHub says was +124 -28 lines may not actually be +124 -28 when using a different diff algorithm! ↩
- You can configure the number of context lines globally with
git config --global diff.context <number>or for individual commands usinggit diff -U<number>. ↩