DiffScope

Merge Conflicts Explained

5 min readUpdated September 2026
Source code projected on a wall

Few things rattle a new developer like the words "merge conflict." But a conflict is not a failure — it is Git being honest. When two branches changed the same lines in incompatible ways, Git refuses to guess which one you meant and hands the decision to you. Understanding what it is really telling you turns a scary moment into a routine one.

Why conflicts happen

Git merges by comparing each branch against the common ancestor they both started from. If one branch changed a region of a file and the other left it alone, Git takes the change automatically. A conflict arises only when both branches edited the same region in different ways, so there is no single answer that respects both. The classic cause is two people editing the same function, but it can also happen when one branch deletes a block another branch modified.

Because conflicts are about overlapping edits, the best way to avoid them is social as much as technical: keep branches short-lived, pull frequently, and split large files that many people touch. The less time two branches spend diverging, the smaller the overlap when they finally meet.

Reading the markers

When Git cannot merge a region, it writes both versions into the file surrounded by markers. Everything between <<<<<<< and ======= is your current branch's version; everything between ======= and >>>>>>> is the incoming version, and each side is labelled with its branch or commit. The markers are ordinary text that Git inserted — your job is to replace that whole block with the correct final content and delete the markers entirely.

The mistake to avoid is leaving a stray marker behind. A forgotten <<<<<<< or ======= will usually break the file, so after resolving, search the whole project for the marker characters to be sure none survived.

The third version you are not shown

A merge is a three-way operation: your branch, the incoming branch, and the common ancestor both of them grew from, which Git locates with merge-base. The default markers show only two of those three, and that is why a conflict can be so hard to judge — you get two versions of a block with no way to tell which side changed what.

Setting merge.conflictStyle to diff3 adds the missing piece: a third region, introduced by a line of ||||||| markers, holding the ancestor’s text. With it a conflict often resolves itself at a glance — one side merely reformatted the block while the other changed its logic, and the ancestor tells you which is which. Git 2.35 added zdiff3, which does the same but first lifts out the lines both sides changed identically, leaving a smaller block to reason about.

Ours and theirs swap during a rebase

In a merge, ours is the branch you are standing on and theirs is the branch arriving. A rebase does the opposite: Git checks out the upstream branch and replays your commits on top of it one at a time, so in a conflict ours is the upstream code and theirs is your own commit being replayed. Cherry-pick behaves the same way. This inversion is the most common reason a resolution comes out backwards.

It matters most when you reach for the blunt instruments. git checkout --ours and --theirs take one whole side of a file and discard the other’s changes completely — reasonable for a lockfile you are about to regenerate, almost never right for source code, and genuinely dangerous if you have the sides the wrong way round. When in doubt, resolve by reading both versions instead of picking a side.

Conflicts Git cannot see, and ones it remembers

A clean merge is not the same as a correct one. If one branch renames a function while another adds a call to the old name, the two edits touch different lines, Git merges them without a word, and the result does not build. The same happens when one side changes a function’s arguments and the other adds a caller. Git compares text; meaning is compared only by you and your tests, so run them after every non-trivial merge.

For the conflicts that keep returning, Git can do the remembering. Enable rerere.enabled and it records how you resolved each conflicted block, then replays that resolution the next time the same block conflicts — which, on a long-lived branch you rebase repeatedly, it will. Generated files are a related special case: resolve a lockfile or a build artefact by regenerating it from the merged sources, never by hand-editing the conflict.

Resolving with a diff tool

The hard part of a conflict is deciding what the merged result should be, and seeing the two versions clearly makes that decision far easier. Copy your side and the incoming side into the two panes of DiffScope — without the marker lines — and compare them directly. The highlighting shows exactly where they diverge, so you can build a final version that keeps the right piece from each, rather than blindly choosing one whole side.

Once you have crafted the resolution, paste it back over the conflicted block, remove the markers, and let the tests confirm the result. Reviewing the before-and-after in a diff one more time is a cheap way to be sure you did not drop a change from either branch.

The takeaway

A merge conflict means both branches changed the same region, so Git asks you to choose. Read the <<<<<<< / ======= / >>>>>>> markers, compare the two sides in a diff to craft the right result, then delete every marker before you commit.

Related tool
DiffScope · Compare tool

Open the tool and try it now