2.2 合并的两种形态与冲突勘查


2.2 合并的两种形态与冲突勘查

本节摘要:merge 只有两种结局——fast-forward(只挪指针,不建对象)与三方合并(新建一个双父提交)。冲突不是错误状态,而是三方比对中 Git 不肯替你做主的地方。本节用真实哈希还原一次冲突现场,教你像验尸一样读 <<<<<<< 标记。

开工前的预备判断

  1. 能判断一次 merge 会走 fast-forward 还是三方合并
  2. 能从 merge commit 的两个 parent 读出合并双方的来源
  3. 能在冲突文件里分清 base、ours、theirs 三方证据
  4. 能熟练完成一次冲突解决并理解 --continue--abort 的分野

合并是分支模型里唯一"双向接触"的操作,也是多数人第一次真正感到 Git 危险的地方。其实把第 2 章 2.1 的公理带进来——提交不可变、引用可随便挪——合并只有两种可能:要么直接挪指针完事,要么新建一个双父提交来缝合两条链。判断走哪条路的证据就一条:双方是否一方是另一方的祖先。下面分别构造现场取证。

一、fast-forward:合并的免费形态

构造最简单的场景——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 扫一遍残留标记,成本一秒,收益是避免把冲突标记发布到主干。

本节要点回顾

  • 两种形态:祖先关系走 fast-forward 只挪指针;双方前进走三方合并新建双 parent 提交。
  • merge-base 是基线:三方比对以公共祖先为 base,单侧改动自动采纳。
  • 冲突即三舞台:1=base、2=ours、3=theirs,裁决后 add 回舞台 0。
  • abort 零成本:未完成的合并不产生对象,随时全身而退。
  • 痕迹取舍--no-ff 保留合并拓扑,是否使用属于团队规范。

指针模型还剩最后一块拼图:当你对历史本身不满意、想把它重写得更直时——变基登场。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U