Version Control

Git Merge and Rebase Conflict Resolution in Practice

For developers branching daily, this guide reads merge conflict markers end to end with reproducible cases and shows how to handle merge, rebase, and rename conflicts.

By LaoHand Team·8 min read·Updated 2026-09-06

Understand What a Conflict Really Is

A conflict is not Git failing; it is two commits editing the same lines differently, and Git cannot decide who wins. It keeps both sides and leaves the call to you. Understanding that takes most of the anxiety out of it.

The usual triggers are merge (merge feature into main) and rebase (rebase feature onto main). Merge combines two histories into one new commit; rebase replays your branch commits on top of the target branch. The symbols and the fixing approach are the same, only the resulting history differs.

Before touching anything, confirm which operation you are in, because it decides whether you finish with git merge --continue or git rebase --continue. git status tells you whether you are Merging or Rebasing.

git merge feature
# -> conflict in src/user.ts

# or

git rebase main
# -> error: could not apply 3a2b1c0 ...

git status
# On branch feature
# You have unmerged paths.
#   both modified: src/user.ts

Reading Conflict Markers: What Each Division Means

A conflict block is split by <<<<<<< , ======= and >>>>>>> . The part between <<<<<<< and ======= is the current branch (HEAD, the side receiving the merge); the part between ======= and >>>>>>> is the incoming commit from the other branch.

Careful: inside a rebase the direction flips. The <<<<<<< side is the commit you are replaying (from your rebased branch), while the section up to >>>>>>> is main. If unsure, git log disambiguates which one is your latest work.

The basic rule: except for path or rename conflicts, most conflicts are resolved by picking one side, merging both, or rewriting entirely. Then strip the markers and the discarded side so only the intended final version remains.

<<<<<<< HEAD            <- 当前分支(merge 时为接收方)
const version = 2;
=======
const version = 3;
>>>>>>> feature        <- 被并入的分支

# 你想取对方的新版本:
const version = 3;

Mistake: Deleting a Line and Silently Regressing

The most dangerous move is getting annoyed at a conflict and deleting one side wholesale. You may keep buggy legacy logic or drop a guard the other branch added, so it runs without errors but behaves wrong. Such regressions often surface weeks later in tests.

Another common mistake is leaving the delimiter: deleting one side but forgetting <<<<<<< or >>>>>>>. That usually breaks syntax immediately, but during a rebase a stray marker can get committed via --continue.

The right habit is reading both sides before choosing. If both matter, hand-merge into a single block; if only one side should stay, strip everything else plus all markers, run tests, then continue the operation.

# 错误示范:删错一边
<<<<<<< HEAD
if (user.balance < cost) throw new Error("insufficient");
=======
if (!user.valid) throw new Error("invalid user");
>>>>>>> feature

# 修复:不是二选一,而是判断完整
if (!user.valid) throw new Error("invalid user");
if (user.balance < cost) throw new Error("insufficient");

Three Common Kinds: Text, Rename, and Semantic Conflicts

Text conflict means both sides edited the same lines; the fix is above. A rename conflict shows a file reported as deleted/modified or rename/rename; git status clarifies it, and it often cannot be auto-merged by text-based Git, so you manually keep a name and update references.

The easiest to miss is the semantic conflict: no markers at all, but the combined changes contradict each other. Say branch A turns config.timeout from seconds into milliseconds while branch B adds a call that computes in seconds. It merges, compiles, and runs — just with wrong numbers.

Semantic conflicts are caught by running tests right after the merge, paying attention to interaction points when skimming git diff, and reviewing the merged branch changes. Do not fixate only on files that carry markers.

# 重命名冲突:一个文件被双方重命名成不同名字
git status
#   both renamed: src/old.ts -> src/newA.ts
#   and src/old.ts -> src/newB.ts

# 保留 A 的名字,B 的内容手动合并后提交新文件
# 删除多余文件并让引用指向最终名
rm src/newB.ts
mv src/newA.ts src/final.ts

Merge vs Rebase: Which Loses Less Information

merge preserves real topology and both sides' commits, resolved once; rebase rewrites your commits for a linear history, but each commit may be replayed and conflicts solved repeatedly. On a shared main you must never rebase it, or everyone else's references break.

On a worktree or a solo branch, rebase gives clean history; but once your branch has been pulled by someone else, merge is safer. The rule of thumb is whether anyone else pulled it already: shared means merge, private left you free to rebase.

Commit any messy work-in-progress before rebasing to avoid loose edits; before --continue, git add the conflict files, run git rebase --continue each step, and finally push with git push --force-with-lease (private branches only).

# 合并风格
git merge main
# ... resolve ...
git merge --continue

# 变基风格(私有分支)
git rebase main
# ... resolve file A ...
git add fileA
git rebase --continue
# ... resolve file B ...
git add fileB
git rebase --continue

git push --force-with-lease origin feature

Verify: Make Sure Nothing Was Silently Lost

Resolving a conflict is not done. Run tests and lint first, then scrutinize the spots you hand-merged — those are the likeliest source of errors.

Skim git diff right after resolving to confirm only your intent remains; git log --merge or git log -p on the touched files catches accidental drops. In daily practice, committing small and often plus syncing the target branch regularly keeps conflicts small in the first place.

# 确认冲突是否全部解决
git status   # 不再出现 unmerged paths 即完成

git diff --stat

git log --oneline --graph -10    # 查看合并后的拓扑

# 验证你手改的那段最终代码
git show HEAD:src/user.ts | grep -n "user.valid"

Official References

Each command links to its official documentation below, so you can verify the latest usage and read deeper.