三方合并必须找公共祖先,因为Git依赖共同基线(merge-base)判断改动是新增还是覆盖,区分谁修改了哪些内容,从而决定自动合并或标记冲突。

三方合并为什么必须找公共祖先
Git 不是简单对比两个分支最新提交的文件差异,而是必须找到一个“参照物”——也就是共同祖先(merge-base)。没有它,就无法判断某行改动是“新增”还是“覆盖”,更无法区分谁改了、谁没动。比如你和同事都基于 commit A 开发,你改了 config.json 的 port 字段,同事改了同一文件的 host 字段,Git 靠 commit A 这个共同基线才能确认:两处修改不重叠,可自动合并;如果你们都改了 port,才标记冲突。
git merge-base 怎么找最近公共祖先
Git 把每个提交看作有向无环图(DAG)中的节点,父指针构成边。找最近公共祖先(LCA)本质是图算法问题:
– 从分支 A 的 HEAD 开始向上遍历所有祖先,存入哈希表
– 再从分支 B 的 HEAD 向上遍历,第一个出现在哈希表里的提交就是 LCA
– 如果存在多个公共祖先(比如多叉合并),Git 默认取“最低”的那个(即离 HEAD 最近的)
常见误区:
– git merge-base A B 返回的是 commit 对象,不是分支名或标签名本身
– 如果两个分支完全无关(比如孤儿分支),merge-base 可能返回空,此时 Git 会退化为“递归合并”并提示“no common ancestor”
– git merge --allow-unrelated-histories 就是为这种场景设计的,但它跳过 LCA 计算,直接把两个历史视为“虚拟祖先为空”
所谓“虚拟公共祖先”其实是 fallback 行为
Git 官方文档里并没有“虚拟公共祖先”这个术语,它只在两种情况下表现出类似行为:
– 使用 --allow-unrelated-histories 时,Git 把两个分支的 root commit 当作隐式共同起点,但不生成真实 commit,只是逻辑上当作 base
– 在 cherry-pick 或 rebase 场景中,如果找不到 LCA,Git 会尝试用 patch-id 比对内容相似性,这时的“base”是人工指定的或空树(4b825dc642cb6eb9a060e54bf8d69288fbee4904,即空树 hash)
– 这些都不是真正的祖先,只是算法兜底策略,不能参与三路 diff 的语义判断
真正可靠的合并,永远依赖真实的 DAG 路径。所谓“虚拟”,往往意味着你已经脱离了 Git 原生协作模型的假设前提。
冲突标记里 <<<<<< HEAD 和 >>>>>> branch-name 的来源
冲突块中的三段内容对应三方:
– <<<<<< HEAD 是当前分支(Ours)在 LCA 之后的修改
– ======= 是分隔线
– >>>>>> branch-name 是待合并分支(Theirs)在 LCA 之后的修改
– 中间缺失的“共同祖先内容”不会显示,但 Git 内部一定用它做过 diff 计算
关键点:
– 如果你在冲突文件里看不到原始 base 内容,不是 Git 没用它,而是为了减少干扰,默认只展示 divergent 部分
– 想查看 base 版本?可以用 git show :1:filename(index stage 1 是 base)
– :2:filename 是 ours,:3:filename 是 theirs


















