← Debugging Guides

阅读中文版本

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.

beginnerdebugginggitmerge

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=U

Interpretation

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.ts

Interpretation

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 -80

Interpretation

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.ts

Interpretation

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.