Git Merge Conflicts
Read conflict markers, choose a correct combined result, and finish the merge with a message that records the decision.
Conflict resolution is integration design
Why Git can merge some edits but not others
Git performs a three-way merge using the merge base, the current branch tip, and the other branch tip. It can combine independent changes automatically. It stops when those inputs do not determine one safe final result.
Independent changes
Main changes the README while feature adds a source file, or both edit clearly separate context Git can combine.
Ambiguous result
Both sides change overlapping lines differently, one deletes what the other edits, or file operations disagree.
Prepare before reproducing a conflict
Use a disposable repository for this lesson. Begin with a clean state and record the graph, merge base, unique commits, and feature patch before merge.
git status
git branch --show-current
git log --oneline --decorate --graph --all
git merge-base main feature/config
git log main..feature/config --oneline
git log feature/config..main --oneline
git diff main...feature/configAlso run the repository’s tests before merging. A failing baseline makes it impossible to attribute later failures to the resolution.
Read the repository while merge is in progress
When merge stops, HEAD still points to the current branch’s pre-merge tip. Git records merge metadata, populates unmerged index stages, and writes conflict markers or file-level states into the working tree.
git status
git diff --name-only --diff-filter=U
git ls-files -u
# Inspect the three indexed stages for one conflicted path
git show :1:src/config.js # merge base
git show :2:src/config.js # ours: current branch
git show :3:src/config.js # theirs: branch being mergedStage 1 · base
The common ancestor version used to determine what each side changed.
Stage 2 · ours
The version from current HEAD—the destination branch in this normal merge.
Stage 3 · theirs
The version from the branch named in the merge command.
Working tree
Git’s attempted merge result, including markers where human intent is still required.
Test what you learned
Type the command that prints the merge-base version of conflicted src/config.js from index stage 1.
Read conflict markers as a question
<<<<<<< HEAD
export const timeout = 3000;
export const retries = 2;
=======
export const timeout = 5000;
export const retries = 4;
>>>>>>> feature/config<<<<<<< HEAD
Begins the current branch’s conflicting region.
=======
Separates current-side content from incoming-side content.
>>>>>>> feature/config
Ends the incoming branch’s conflicting region.
Final result
May use ours, theirs, both, or new content. Remove all markers and express intended current behavior.
The labels do not explain why each side changed. Inspect the commits and surrounding code. A timeout and retry policy may need a third result agreed by current requirements rather than either literal block.
Recover intent before editing
git log --oneline --left-right main...feature/config
git show <main-side-commit> -- src/config.js
git show <feature-side-commit> -- src/config.js
git blame <merge-base> -- src/config.js
# Compare each side with the base:
git diff <merge-base> main -- src/config.js
git diff <merge-base> feature/config -- src/config.js- 01Name the invariant
What behavior must the integrated program preserve?
- 02Read both reasons
Inspect commit messages, tests, issues, and neighboring changes from each side.
- 03Design the final content
Write the current intended behavior, even if neither side contains it verbatim.
- 04Check dependent paths
A textual conflict may imply updates to tests, docs, schemas, generated files, or callers.
Resolve, stage, and inspect
# Edit each conflicted file to the intended final content
git diff
# Mark resolved paths by staging their final content
git add src/config.js
# Prove no unmerged entries or markers remain
git diff --name-only --diff-filter=U
git diff --cached
git diff --cached --check
# Run relevant tests, then finish
git merge --continueStaging a path tells Git that its current working content is the proposed resolution. It does not certify correctness. The unmerged stages are replaced by an ordinary stage-0 index entry, which must be reviewed like any other commit proposal.
Use whole-side selection only when it matches intent
# Replace one conflicted working file with a complete side:
git restore --ours -- src/config.js
git restore --theirs -- src/config.js
# Then inspect, edit if needed, and stage deliberately:
git diff -- src/config.js
git add src/config.jsThese commands replace the whole conflicted file in the working tree. They do not merge individual hunks, explain product intent, stage automatically, or prove tests. Use them only when one complete side is the correct file-level result.
Recognize conflicts beyond markers
Both modified
Both sides changed overlapping content. Inspect all three stages and produce a combined file.
Add/add
Both sides created the same path differently. Choose or combine content and verify why one path should represent both intentions.
Modify/delete
One side removed a path while the other edited it. Decide whether the integrated product still needs the file and where its behavior belongs.
Rename-related
One or both sides renamed a path while changing related content. Confirm final names, imports, references, and duplicate leftovers.
Binary conflict
Line markers may not help. Select or regenerate the intended artifact using source, ownership, and project policy.
File/directory conflict
One side’s file collides with another side’s directory path. Design a valid final tree and update references.
Resolve file-level states explicitly
For a modify/delete conflict, status names which side deleted and which modified. Do not assume deletion wins because there are no ordinary markers.
git status
git ls-files -u -- path/to/file
# Keep an edited/restored final file:
git add path/to/file
# Or confirm deletion in the final tree:
git rm path/to/file
git diff --cached --name-statusWhen keeping behavior under a new path, stage the complete rename/add/delete shape and check every import, link, and test that refers to the old location.
Abort when the correct result is unknown
git status
git merge --abort
git status
git log --oneline --decorate --graph --allAbort attempts to reconstruct the pre-merge state. It is most reliable when the merge began with a clean working tree and index. Complex pre-existing local changes can make exact reconstruction harder, which is another reason to preserve them before merging.
Verify syntax, semantics, and history
INDEXNo unmerged entriesUnmerged-path filters and
git ls-files -uproduce no entries.TEXTNo markers or whitespace errorsSearch tracked source and run cached diff check before committing.
BEHAVIORRelevant tests passCover both branch intentions and the integration boundary, not only syntax.
HISTORYMerge parents are correctThe final merge commit names the expected destination and feature tips as parents.
STATEWorking tree is cleanNo accidental files, unresolved paths, or unrelated edits remain.
git diff --name-only --diff-filter=U
git ls-files -u
git diff --cached --check
git grep -n -e '^<<<<<<< ' -e '^=======$' -e '^>>>>>>> '
# After tests and merge completion:
git show --no-patch --pretty=raw HEAD
git show --stat --first-parent HEAD
git statusThe marker search is a useful guard, not universal proof: legitimate documentation can contain marker-like examples. Review every match in context.
Write a merge message that records the decision
A useful merge message names the integrated branch or capability and explains non-obvious resolution choices. Do not claim “resolved conflicts” without stating which behavior was preserved and why.
fix merge
Does not identify the feature, conflict, invariant, or verification.
Record the integration choice
“Merge checkout timeout policy. Keep main’s cancellation signal and feature’s bounded retry count; integration tests cover both paths.”
Optional: reuse recorded resolutions carefully
git rerere can record how a conflict shape was resolved and reuse that resolution when the same conflict reappears. Enable it only when you understand that reused content still requires review and tests.
git config rerere.enabled true
git rerere status
git rerere diffRerere reduces repeated typing; it does not know whether product requirements changed since the earlier resolution.
Guided practice: resolve a real conflict
Use a disposable repository. Create a base configuration and test, then branch. On main, change timeout handling and its test. On feature, change retry behavior in the same function and its test.
- 01Record both intentions
Inspect each branch’s unique commit and prove its tests pass independently.
- 02Predict the conflict
Compare each side with the merge base and identify overlapping lines before merging.
- 03Trigger and inspect
Merge into main, read status, list unmerged stages, and print base/ours/theirs.
- 04Design combined behavior
Preserve cancellation and bounded retries, update tests, remove markers, and stage exact paths.
- 05Verify and continue
Run marker checks, cached review, all tests, continue, inspect both parents, and prove clean status.
Independent lab: resolve four conflict classes
Create a disposable repository with separate scenarios for:
- A same-line content conflict requiring a combined result.
- An add/add conflict where both files contain wanted sections.
- A modify/delete conflict where behavior moves to a replacement path.
- A rename-related conflict requiring updated imports and no duplicate old path.
- For every scenario, capture the pre-merge graph, merge base, unique commits, three index stages where available, final cached diff, tests, and merge parents.
- Abort one first attempt deliberately, prove restoration, then rerun and resolve from clarified requirements.
- Finish with clean status and a short resolution log stating the invariant preserved in each merge.
Common conflict-resolution mistakes
Deleting markers and stopping
Marker removal is not semantic resolution. Inspect, stage, test, and verify history.
Choosing a side by label
Ours/theirs describe operation roles, not quality or ownership.
Ignoring the merge base
The base reveals what each side changed and prevents mistaking old shared content for one side’s intent.
Checking only conflicted files
Auto-merged dependent files can still produce invalid combined behavior.
Committing unrelated edits
Keep the merge resolution focused. Review the complete index before continuing.
Forcing through uncertainty
Abort, clarify requirements, and add tests rather than inventing an unsupported result.
Lesson review
You can now treat merge conflicts as three-way integration problems. The base, current side, and incoming side provide evidence; the final file expresses present intent; the staged tree, tests, parent graph, and clean status prove the resolution.
- I can inspect stage 1, 2, and 3 versions of an unmerged path.
- I read commit intent and dependent behavior before editing markers.
- I resolve file-level conflicts as deliberate final-tree decisions.
- I know when to continue and when to abort the merge.
- I verify unmerged entries, markers, cached content, tests, parents, and final status.