本节摘要:merge 只有两种结局——fast-forward(只挪指针,不建对象)与三方合并(新建一个双父提交)。冲突不是错误状态,而是三方比对中 Git 不肯替你做主的地方。本节用真实哈希还原一次冲突现场,教你像验尸一样读
<<<<<<<标记。
--continue 与 --abort 的分野合并是分支模型里唯一"双向接触"的操作,也是多数人第一次真正感到 Git 危险的地方。其实把第 2 章 2.1 的公理带进来——提交不可变、引用可随便挪——合并只有两种可能:要么直接挪指针完事,要么新建一个双父提交来缝合两条链。判断走哪条路的证据就一条:双方是否一方是另一方的祖先。下面分别构造现场取证。
构造最简单的场景——dev 从 main 分叉后,main 原地不动:
git switch -c dev printf "a\n" > a.txt; git add .; git commit -m "dev-1" printf "b\n" > b.txt; git add .; git commit -m "dev-2" git switch main git merge dev # Updating a1b2c3d..e5f6a7b # Fast-forward # create mode 100644 a.txt ...
注意输出里没有 "Merge made by"字样——没有创建任何提交。取证验证:
cat .git/refs/heads/main # 与 dev 指向同一哈希 e5f6a7b git cat-file -p HEAD # 单 parent,就是 dev-2
fast-forward 的本质:main 是 dev 的祖先,合并只需把 main 这个 41 字节文件里的哈希改写为 dev 的哈希。零新对象、零冲突可能。你可以用 git merge --no-ff dev -m "merge dev" 强制走三方合并,保留"这里发生过一次分支汇合"的拓扑痕迹——是否这么做是团队规范问题,第 5 章再谈。
真正的合并发生在双方各自前进了的情况。设 main 上有 M、dev 上有 D、公共祖先是 B。Git 的三方合并算法以 B 为基线:某文件只有 M 改了就采纳 M,只有 D 改了就采纳 D,双方都改且改得不同才冲突。先用 merge-base 命令找到 B:
git merge-base main dev # 输出公共祖先哈希
构造一个冲突现场并合并:
# main 上:把 README 第一行改成 "from main" git switch main; sed -i '1s/.*/from main/' README.md git commit -am "main edit" # dev 上:同一行改成 "from dev" git switch dev; sed -i '1s/.*/from dev/' README.md git commit -am "dev edit" git switch main git merge dev # Auto-merging README.md # CONFLICT (content): Merge conflict in README.md # Automatic merge failed; fix conflicts and then commit the result.
此刻勘查现场。冲突文件里的标记:
<<<<<<< HEAD from main ======= from dev >>>>>>> dev
三段证据:<<<<<<< HEAD 到 ======= 是 ours(当前分支 main 的版本),======= 到 >>>>>>> dev 是 theirs(被合进来的 dev 版本)。base 版本不在文件里,要看它得自己取证:
git show :1:README.md # 冲突前 base 公共祖先版本 git show :2:README.md # ours git show :3:README.md # theirs
冒号加数字是 Git 的"舞台编号"语法,冲突期间三份证据同时登记在 index 里——这在对象模型侦探眼里非常优雅:冲突状态本身就是三棵 tree 并存的特殊 index,解决冲突就是把舞台 2 和 3 的分歧整理成一份,git add 后登记回舞台 0。
手工编辑(保留正确内容、删除全部标记)或用检查工具逐个裁决:
git checkout --theirs README.md # 全采对方版 git checkout --ours README.md # 全采本方版 git mergetool # 三栏比对工具(需配置)
裁决完毕后收尾:
git add README.md git commit # 或 git merge --continue,会自动生成默认提交说明
生成的 merge commit 有两个 parent。验尸:
git cat-file -p HEAD # tree ... # parent <main 原顶点> # parent <dev 顶点> # Merge branch 'dev' git log --graph --oneline # * 7g8h9i0 Merge branch 'dev' # |\ # | * dev edit # * | main edit # |/ # * 公共祖先
反悔则 git merge --abort,现场完全复原,就像合并从未发生——因为未完成的合并只是 index 处于特殊三舞台状态,对象库里还没写进任何合并提交,丢弃它零成本。

默认策略对双分支各自前进的场景是 ort(新版的递归三方合并),它会在合并前自动做"虚拟公共祖先"以处理多次分叉的复杂情况。日常不需要手动选策略;偶尔在保留目录结构语义时用到 git merge -s ours(合入历史但完全保留本地内容)这类特殊形态,属于进阶武器,用前务必团队对齐。我的建议是:冲突解决原则写进团队规范——"谁最后改动谁负责联调",比任何策略参数都管用。
⚠️ 常见坑:解决冲突时只删了
<<<<<<<却忘了>>>>>>>,或留下空行污染语义化 diff。提交前用git diff --check扫一遍残留标记,成本一秒,收益是避免把冲突标记发布到主干。
--no-ff 保留合并拓扑,是否使用属于团队规范。指针模型还剩最后一块拼图:当你对历史本身不满意、想把它重写得更直时——变基登场。