三方合并
三方合并用一个共同的旧快照比较两个分支末端。当前检出侧叫 ours,命令指定的源侧叫 theirs,最佳共同祖先叫 merge base。基点提供修改前的状态,让 Git 判断哪一侧改过、哪一侧没改。
提交图负责选择基点并判断末端关系;三个快照负责逐路径决定内容。所有路径都得到无冲突结果后,引擎才写入结果树、双父提交,并推进当前分支。若冲突,这些最终写入都不会发生。
四条路径规则
对一个路径,比较 base、ours、theirs:
| 条件 | 结果 |
|---|---|
ours == theirs | 两侧相同,取任一侧 |
ours == base | 只有源侧修改,取 theirs |
theirs == base | 只有当前侧修改,取 ours |
| 三者互不相同 | 冲突,停止 |
两侧独立改成相同值时,即使与 base 不同,第一条也直接接受。本课用两个平面文本文件演示整文件值比较。真实 Git 还处理目录、模式、删除、重命名、二进制、驱动和策略选项,但 base 始终提供共同的修改前状态。
三方合并逐步演示
默认轨迹如下:
commit snapshot "init": app.py=print("v1"), README.md=hello
git branch feature
commit snapshot "main-api": app.py=print("v2"), README.md=hello
git switch feature
commit snapshot "feature-docs": app.py=print("v1"), README.md=quickstart
git switch main
git merge feature
使用固定身份和时间的 Git 格式 SHA-1 对象,让变化集中在树和父提交。
- 根快照形成树
a7c4b8a...和提交6bb158e...,main 首次指向它。 - 创建 feature,在
6bb158e...新增引用,不复制提交,HEAD 仍在 main。 main-api只改 app.py,形成提交fb177c6...,父为6bb158e...,只推进 main。- HEAD 切到仍在基点的 feature。
feature-docs改 README,app.py 与 base 相同,形成兄弟提交87fb417...。 - 切回 main,合并固定
ours = fb177c6...、theirs = 87fb417...,比较期间不移动输入。 - 祖先探测先标记 ours 可达提交,再遍历源历史,得到唯一最佳共同祖先
6bb158e...。 - base 不等于任一末端,既不是已包含,也不能快进,载入三个快照。
- app.py 的 theirs 与 base 都为
print("v1"),按第三条取 ours 的print("v2")。 - README 的 ours 与 base 都为
hello,按第二条取 theirs 的quickstart。 - 结果为 app.py=
print("v2")、README=quickstart,序列化成树2bcb0a8...。 - 提交正文引用结果树,第一父为旧 HEAD
fb177c6...,第二父为源末端87fb417...,得到提交73bb817...。 - 最后 main 进到
73bb817...,feature 留在87fb417...。新增一个提交对象,当前引用写一次,源引用不写。
父顺序记录命令的视角。即使结果树相同,交换两条 parent 行也会改变提交 ID,因为有序头部参与哈希;first-parent 历史遍历也会选择不同主线。
为什么必须选最佳共同祖先
遍历先遇到的共同祖先不一定最佳。一个共同祖先可能还有更新的共同后代,这时应使用后者。本引擎收集共同祖先,删除那些仍是其他共同候选祖先的旧候选。
普通分叉只剩一个最佳基点;交错合并可能留下多个。本可视化在此报告模型边界,而非说历史非法。真实 Git 的 ort 等策略可以把多个基点合成为虚拟基点后,再合并末端。使用过旧基点会把错误的“之前状态”交给路径规则。
边界情况
- base 等于 ours:当前为源祖先,移动当前引用即可快进,不需要三快照协调或合并提交,工作台不打开。
- base 等于 theirs:当前已包含源,报告已是最新;即使名称指向不同提交,也不写对象或引用。
- 两侧同改:ours 与 theirs 相同,第一条接受,base 可不同。
- 同一路径三值不同:本引擎在结果树写入前停止。真实 Git 为该路径存索引 stage 1、2、3,设置
MERGE_HEAD等合并状态,工作文件可能写入冲突标记;解决并提交后才有合并提交。 - 多个最佳基点:交错历史可能发生,本模型停止,真实策略可合成虚拟基点。
- 八提交上限:只是布局限制。轨迹报告超出范围,不存拟议提交,不表示真实 Git 拒绝大仓库。
常见错误
- “三方是三条分支。” 三方是 base、ours、theirs 三个快照,命令只涉及两个末端。
- “第一个共同祖先就是基点。” 还要排除有更新共同后代的旧候选。
- “ours 总赢。” theirs 未改才取 ours;ours 未改就取 theirs;两侧相同则接受共同值。
- “冲突会创建冲突提交。” 合并停在未解决索引状态,解决所有路径后才创建正常结果提交。
- “合并推进两条分支。” 只推进当前检出的目标分支,源不动。
- “合并提交存补丁。” 它引用完整结果树和两个父提交,差异由快照之间计算。
- “父顺序只影响显示。” 顺序属于对象字节,会影响 ID 和第一父历史。
模型范围
本课有干净本地仓库、附着 HEAD、两个 100644 平面文件、确定性 SHA-1 对象,最多三个分支和一个唯一最佳基点。路径合并按完整字符串值比较。省略干净合并中的工作区和索引、重命名、删除、模式、二进制驱动、自定义属性、钩子、签名、标签、远程、子模块、章鱼合并、策略选项、reflog、并发引用更新及递归虚拟基点构造。
实际行为见 git merge、git merge-base、git read-tree。