Git merge conflict — Resolve Markers Without Losing Changes
Diagnose and resolve Git merge conflicts by listing unmerged paths, reading both sides, choosing a semantic resolution, and verifying with tests before committing.
Symptoms
- Automatic merge failed; fix conflicts and then commit the result.
- git status shows both modified: src/app.ts
- The file contains <<<<<<< HEAD, =======, and >>>>>>> feature/login markers
- A rebase or cherry-pick stops with CONFLICT
Possible causes
- Two branches changed the same lines of a file
- One branch renamed or deleted a file the other branch edited
- A binary file such as an image or lockfile changed on both sides
- Line endings or formatting changes made every line look modified
Diagnosis steps
List every unmerged path
git status --short && git diff --name-only --diff-filter=UInterpretation
UU means both sides modified, AA means both added, DU and UD mean delete versus modify. The list is the complete scope; resolving only files reported by the editor leaves the merge blocked.
Read the conflict hunks with context
git diff -- src/app.tsInterpretation
The diff shows the base, ours, and theirs sections. Read the surrounding function to understand intent rather than picking a side by line number.
Inspect both versions independently
git show :2:src/app.ts > /tmp/ours.ts && git show :3:src/app.ts > /tmp/theirs.ts && diff -u /tmp/ours.ts /tmp/theirs.ts | head -80Interpretation
Stage 2 is ours (current branch) and stage 3 is theirs (incoming). During a rebase these meanings swap relative to the branch names, so check the operation first.
Check for tooling or formatting noise
git diff --stat && git config --get core.autocrlf && git check-attr text eol -- src/app.tsInterpretation
A conflict touching every line usually means line-ending or formatter differences. Fix the normalization separately instead of hand-merging thousands of lines.
Root cause
Git merges at the line level using the common ancestor recorded in the merge base. When both branches change overlapping lines, Git cannot choose a result and writes conflict markers into the working tree, leaving the path unmerged in the index. The merge or rebase cannot continue until every unmerged path is resolved and staged.
Fix
Resolve each hunk by combining the intent of both sides, remove all conflict markers, then stage the file with git add and complete the merge with git commit. Use git checkout --ours or --theirs only when one side is genuinely correct, and re-run tests after resolution. If the merge is not worth completing, git merge --abort restores the pre-merge state; git rebase --abort does the same for a rebase.
Risk note
Blindly accepting --ours or --theirs silently discards the other branch's work. Committing a file that still contains conflict markers breaks the build. During a rebase, ours is the branch being rebased onto, not your feature branch, so the flags are easy to invert.
Verify the fix
Run git diff --check to catch leftover markers and grep the tree for <<<<<<<. Confirm git status lists no unmerged paths, run the affected test suite, and inspect the final diff before committing the merge.
A conflict is a missing decision
Git can merge changes that touch different lines. When both sides modify the same region, it stops and writes both versions into the file:
<<<<<<< HEAD
const timeout = 30_000;
=======
const timeout = 60_000;
>>>>>>> feature/slow-api
The file is now syntactically invalid and the path is unmerged in the index. Nothing is committed until you decide what the combined code should be.
See the full scope
git status --short
git diff --name-only --diff-filter=U
git diff -- src/app.ts
UU is a content conflict, DU means deleted by us and modified by them, AA means both added. Handle rename and delete conflicts explicitly; they are easy to miss when you only look at files with markers.
Resolve by intent
Read the surrounding code and choose a result that preserves both behaviors, which may be neither side verbatim:
const timeout = env.API_TIMEOUT_MS ?? 60_000;
For a generated lockfile, regenerate it (pnpm install, npm install) instead of editing markers. For line-ending noise, normalize with .gitattributes and reapply the change. Use git checkout --ours -- path only when you can explain why the other side should be discarded.
Verify before committing
grep -rn '^<<<<<<<\|^=======$\|^>>>>>>>' src/ || true
git diff --check
git add src/app.ts
git commit
Run the tests that cover the merged area. A clean git status and a passing suite are the real proof; no conflict markers in the diff is the minimum. If the result is worse than either branch, git merge --abort is still available before the commit.