Skip to main content
Published / updated

Merge Conflicts

Before you start

You need: branching (Article 03).

Time: about 50 minutes, at the keyboard. Create a real conflict — reading about them teaches nothing.

Learning objective

Resolve a merge conflict correctly — keeping both changes where both are needed — and verify the result before committing.

Topics

  • Why conflicts happen
  • Reading conflict markers
  • Resolving by hand
  • --ours, --theirs and when they are wrong
  • Aborting
  • Conflicts during rebase
  • Verifying a resolution
  • Preventing conflicts

Why conflicts happen

Git merges automatically when changes are in different places. It only asks you when it cannot decide.

SituationResult
Different files changedMerges cleanly
Different regions of one fileMerges cleanly
Same lines changed on both sidesConflict
Deleted on one side, edited on the otherConflict
Renamed differently on each sideConflict

A conflict is not a failure. It is Git refusing to guess, because guessing would silently lose one person's work.

The cost of a conflict scales with how long branches diverge. A branch merged daily rarely conflicts; a branch open for three weeks conflicts on every file the team touched.

Reading the markers

public decimal CalculateTotalFees(int studentId)
{
<<<<<<< HEAD
decimal total = _feeRepository.GetTotalFees(studentId);
decimal discount = _discountService.GetDiscount(studentId);
return total - discount;
=======
decimal total = _feeRepository.GetTotalFees(studentId);
decimal lateFee = _lateFeeService.CalculateLateFee(studentId);
return total + lateFee;
>>>>>>> feature/late-fees
}
MarkerMeaning
<<<<<<< HEADStart of your version — the branch you are on
=======Divider
>>>>>>> feature/late-feesEnd of their version — the branch being merged

In this example, taking either side is wrong. One loses the discount, the other loses the late fee. The correct resolution contains both:

public decimal CalculateTotalFees(int studentId)
{
decimal total = _feeRepository.GetTotalFees(studentId);
decimal discount = _discountService.GetDiscount(studentId);
decimal lateFee = _lateFeeService.CalculateLateFee(studentId);
return total - discount + lateFee;
}

This is the central point of the article. The tempting move — "keep mine" or "keep theirs" — quietly deletes a working feature. Nothing fails, no test necessarily breaks, and the loss surfaces weeks later when the accounts team asks why late fees stopped appearing.

Always read both sides and ask what each was trying to do. If you do not know why the other change exists, ask the person who wrote it. git log on the file tells you who.

Resolving

git merge feature/late-fees
Auto-merging src/Services/FeeService.cs
CONFLICT (content): Merge conflict in src/Services/FeeService.cs
Automatic merge failed; fix conflicts and then commit the result.
git status
Unmerged paths:
both modified: src/Services/FeeService.cs
git diff # shows the conflicted regions
git log --merge -p src/Services/FeeService.cs # the commits that conflict

git log --merge is underused: it shows only the commits involved in the conflict, with their messages — which is often enough to understand the other side's intent without asking anyone.

Then:

  1. Open the file.
  2. Read both sides.
  3. Write the correct combined code.
  4. Delete all three markers.
  5. Build and test.
  6. git add the file.
  7. git commit.
git add src/Services/FeeService.cs
git status # confirm nothing is still unmerged
git commit # message is pre-filled

A leftover <<<<<<< is a compile error you committed. Search before committing:

git diff --staged | grep -E "^\+(<<<<<<<|=======|>>>>>>>)"
grep -rn "<<<<<<<" src/

Ours and theirs

git checkout --ours src/Services/FeeService.cs # keep your version entirely
git checkout --theirs src/Services/FeeService.cs # keep theirs entirely
git add src/Services/FeeService.cs

These discard the other side completely. They are correct in narrow cases:

CaseChoice
A generated file (lock file, migration snapshot)Regenerate rather than resolve
A file you deliberately rewrote wholesale--ours
Taking a colleague's replacement of a file you barely touched--theirs
Two real features in the same methodNeither — merge by hand

During a rebase, --ours and --theirs are reversed relative to intuition, because rebase replays your commits onto their branch — so "ours" is the branch you are replaying onto. This trips up experienced developers. When rebasing, read the content rather than trusting the flag name.

Aborting

git merge --abort # undo the merge entirely
git rebase --abort
git cherry-pick --abort

--abort restores the state before the merge started. Nothing is lost. If a conflict is larger than expected, or you realise you are on the wrong branch, abort, think, and start again.

Aborting is not failure. Half-resolving a large conflict at the end of the day is how bad resolutions get committed.

Conflicts during rebase

git rebase main
CONFLICT (content): Merge conflict in src/Services/FeeService.cs
error: could not apply 8c2e4f1... Add late fee calculation
# resolve the file
git add src/Services/FeeService.cs
git rebase --continue # not git commit
git rebase --skip # drop this commit
git rebase --abort # give up

Rebase conflicts arrive one commit at a time. A rebase of ten commits can stop ten times, and you may resolve the same region repeatedly as each commit replays.

That is the practical argument for merging rather than rebasing a long-lived branch: a merge asks once.

git config --global rerere.enabled true

rerere records how you resolved a conflict and replays it if the same conflict appears again. It pays for itself on any repeated rebase.

Verifying

Resolving the markers is not the same as resolving the conflict.

Build. A resolution that does not compile is common — a variable used on one side does not exist on the other.

Run the tests. Especially the ones covering both features.

Read the whole method, not just the conflicted lines. A change fifteen lines above may now be inconsistent with what you kept.

Check both features still work. In the fee example: a student with a discount, a student with a late fee, and a student with both.

git diff HEAD~1 # what the merge commit actually changed
git log --oneline --graph -10

A conflict resolution is the highest-risk change in normal Git use, because it is the one place where working code disappears without any error. Treat it as a change requiring review, not as bookkeeping.

Preventing conflicts

PracticeEffect
Short-lived branchesLess divergence, fewer conflicts
Pull from main dailyConflicts arrive small
Small commitsSmaller conflicted regions
Agreed formatting (.editorconfig)No conflicts caused by reformatting
One person per area where possibleFewer collisions
Talk before large refactorsAvoids the unwinnable conflict

A formatter run across a file is the worst thing to merge, because every line conflicts and the real change is invisible inside it. Commit formatting separately from behaviour, always.

git config --global merge.conflictstyle zdiff3

zdiff3 adds the original common ancestor between the two sides:

<<<<<<< HEAD
return total - discount;
||||||| base
return total;
=======
return total + lateFee;
>>>>>>> feature/late-fees

Seeing the base makes the intent of each side obvious — one subtracted, one added, both from the same starting point. It is the single best conflict-resolution setting.

Diagnosing

SymptomCause
Build fails after a mergeMarkers left in the file, or an incomplete resolution
A feature vanished after a mergeTook one side wholesale
commit refusesConflicted files not staged
git rebase --continue says nothing to commitResolution ended up empty — --skip
Every line conflictsLine endings or a reformat
The same conflict repeatedly during rebaseNormal — enable rerere

Errors you will hit

What you seeCauseFix
Build fails after a mergeConflict markers left in the fileSearch for <<<<<<<
A feature vanished after a mergeTook one side wholesaleResolve keeping both
git commit refusesConflicted files not stagedgit add them
git rebase --continue says nothing to commitResolution ended up empty--skip
Every line conflictsLine endings or a reformatCommit formatting separately
The same conflict repeatedly during rebaseNormal for a multi-commit rebaseEnable rerere

Taking one side wholesale is the dangerous resolution. Nothing fails, no test necessarily breaks, and a working feature is simply gone.

Common mistakes

  • Taking "ours" or "theirs" without reading both
  • Committing conflict markers
  • Not building after resolving
  • Not testing both features
  • Assuming --theirs means the same thing in a rebase
  • Resolving a huge conflict rather than aborting and rethinking
  • Merging a reformat with a behaviour change
  • Letting a branch diverge for weeks
  • git commit instead of git rebase --continue

Practice

The course exercise is resolve a conflict.

  1. Create a conflict deliberately: change the same line on two branches, then merge.
  2. Read the markers and identify which side is which.
  3. Recreate the fee example — discount on one branch, late fee on the other. Resolve it by taking only one side, then explain what broke.
  4. Resolve it again correctly, keeping both.
  5. Leave a marker in the file and try to build. See the error.
  6. Add the marker search to your routine: grep -rn "<<<<<<<" src/.
  7. Enable merge.conflictstyle zdiff3 and create the same conflict again. Compare readability.
  8. Use git log --merge -p <file> to read the commits behind a conflict.
  9. Start a merge, hit the conflict, and git merge --abort. Confirm the state is exactly as before.
  10. Create a conflict during a rebase of three commits and resolve each with --continue.
  11. During that rebase, check what --ours refers to. Confirm it is the opposite of the merge case.
  12. Enable rerere, resolve a conflict, reset, and repeat the merge. Watch it resolve itself.
  13. Reformat a whole file on one branch and change one line on another. Merge, and see why formatting belongs in its own commit.
  14. Resolve a conflict, then run the tests for both features before committing.

Exercise 3 is the important one. Do it once so that losing a feature to a careless resolution is something you have already seen.

You can now

  • Read conflict markers and tell the sides apart
  • Resolve keeping both changes where both are needed
  • Build and test before committing a resolution
  • Abort a merge that is larger than expected
  • Say why --ours means the opposite during a rebase

Review questions

  1. What does Git merge automatically, and when does it stop and ask?
  2. Why is taking one side wholesale usually the wrong resolution?
  3. Why do --ours and --theirs mean the opposite during a rebase?
  4. What must you do after removing the markers but before committing?

Next: Pull requests and code review