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,--theirsand 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.
| Situation | Result |
|---|---|
| Different files changed | Merges cleanly |
| Different regions of one file | Merges cleanly |
| Same lines changed on both sides | Conflict |
| Deleted on one side, edited on the other | Conflict |
| Renamed differently on each side | Conflict |
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
}
| Marker | Meaning |
|---|---|
<<<<<<< HEAD | Start of your version — the branch you are on |
======= | Divider |
>>>>>>> feature/late-fees | End 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:
- Open the file.
- Read both sides.
- Write the correct combined code.
- Delete all three markers.
- Build and test.
git addthe file.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:
| Case | Choice |
|---|---|
| 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 method | Neither — 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
| Practice | Effect |
|---|---|
| Short-lived branches | Less divergence, fewer conflicts |
Pull from main daily | Conflicts arrive small |
| Small commits | Smaller conflicted regions |
Agreed formatting (.editorconfig) | No conflicts caused by reformatting |
| One person per area where possible | Fewer collisions |
| Talk before large refactors | Avoids 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
| Symptom | Cause |
|---|---|
| Build fails after a merge | Markers left in the file, or an incomplete resolution |
| A feature vanished after a merge | Took one side wholesale |
commit refuses | Conflicted files not staged |
git rebase --continue says nothing to commit | Resolution ended up empty — --skip |
| Every line conflicts | Line endings or a reformat |
| The same conflict repeatedly during rebase | Normal — enable rerere |
Errors you will hit
| What you see | Cause | Fix |
|---|---|---|
| Build fails after a merge | Conflict markers left in the file | Search for <<<<<<< |
| A feature vanished after a merge | Took one side wholesale | Resolve keeping both |
git commit refuses | Conflicted files not staged | git add them |
git rebase --continue says nothing to commit | Resolution ended up empty | --skip |
| Every line conflicts | Line endings or a reformat | Commit formatting separately |
| The same conflict repeatedly during rebase | Normal for a multi-commit rebase | Enable 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
--theirsmeans 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 commitinstead ofgit rebase --continue
Practice
The course exercise is resolve a conflict.
- Create a conflict deliberately: change the same line on two branches, then merge.
- Read the markers and identify which side is which.
- Recreate the fee example — discount on one branch, late fee on the other. Resolve it by taking only one side, then explain what broke.
- Resolve it again correctly, keeping both.
- Leave a marker in the file and try to build. See the error.
- Add the marker search to your routine:
grep -rn "<<<<<<<" src/. - Enable
merge.conflictstyle zdiff3and create the same conflict again. Compare readability. - Use
git log --merge -p <file>to read the commits behind a conflict. - Start a merge, hit the conflict, and
git merge --abort. Confirm the state is exactly as before. - Create a conflict during a rebase of three commits and resolve each with
--continue. - During that rebase, check what
--oursrefers to. Confirm it is the opposite of the merge case. - Enable
rerere, resolve a conflict, reset, and repeat the merge. Watch it resolve itself. - Reformat a whole file on one branch and change one line on another. Merge, and see why formatting belongs in its own commit.
- 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
--oursmeans the opposite during a rebase
Review questions
- What does Git merge automatically, and when does it stop and ask?
- Why is taking one side wholesale usually the wrong resolution?
- Why do
--oursand--theirsmean the opposite during a rebase? - What must you do after removing the markers but before committing?