Published on

搞懂 Git 的三路合并

Authors

从一个冲突说起

合并代码时打开冲突文件,你会看到这样的标记:

<<<<<<< HEAD
当前分支的内容
=======
被合并分支的内容
>>>>>>> feature

但 Git 凭什么判断这是冲突、又凭什么能自动合并另一部分?答案藏在三个文件版本里:BASELOCALREMOTE

三个版本

BASE —— 共同祖先

两个分支分叉之前那一刻,文件的内容。它是合并的参考基准:只有相对 BASE 发生了变化,才算一次"修改"。

LOCAL —— 当前分支

你现在所在分支上的文件内容。也就是 HEAD

REMOTE —— 被合并方

即将合并进来的那个分支上的文件内容。

三路判定的逻辑

Git 拿这三个版本做两次比较:

  1. BASE vs LOCAL改了什么;
  2. BASE vs REMOTE对方改了什么。

然后分情况:

你的修改对方的修改结果
改了 A 行没改 A 行采纳你的
没改 A 行改了 A 行采纳对方的
改了 A 行改了 A 行(不同)冲突,需人工裁决
都没改都没改保持 BASE

关键在于:合并器并不直接拿 LOCALREMOTE 硬碰硬,而是借助 BASE 区分"谁真的动了这一行"。这就是"三路"的由来——两路无法区分"你改了"还是"对方改了"。

Merge vs Rebase:LOCAL/REMOTE 会互换

这是最容易踩的坑。同样两条命令,角色完全相反:

普通合并 git merge

git checkout feature
git merge main
# LOCAL  = feature   (你当前所在的分支)
# REMOTE = main      (被合并的分支)

变基 git rebase

git checkout feature
git rebase main
# LOCAL  = main      (目标基线)
# REMOTE = feature   (你正在 rebase 的分支)

为什么会反过来?因为 rebase 是把 feature 的提交逐个重放到 main 之上。每一次重放,都是把"一个 feature 提交"合并到"已经 rebase 过的 main"上——于是 base 是 main,被合并进来的反而是 feature。LOCAL/REMOTE 的标签自然就互换了。

💡 在 git mergetool 里看到冲突方向"和上次相反"时,先确认你是在 merge 还是 rebase。

分叉结构

下面这张图展示了 BASE 的位置——它是两条分支最近的共同祖先:

        A---C  (main)
       /
...-- B       <- BASE(共同祖先)
       \
        D---E  (feature)

B 就是 BASE。合并 featuremain 时,Git 比较的是 B→CB→E 这两条变化路径。

用 mergetool 处理冲突

git mergetool

它会为每个冲突文件准备四个版本:

file.LOCAL     # 当前分支版本
file.REMOTE    # 要合并进来的版本
file.BASE      # 共同祖先版本
file           # 你要写入的合并结果

打开三方合并工具(如 vimdiff、meld)时,同时看到 LOCAL、BASE、REMOTE 三个窗口会大有帮助——BASE 能告诉你"原本是什么样",从而判断两侧各自的改动意图。

减少冲突的几条实践

  1. 小步快跑:提交粒度小,冲突范围也小。
  2. 频繁同步:长期分叉的分支,合并时几乎必然大面积冲突;定期 merge/rebase 主干。
  3. 约定改谁:对同一模块,同一时间尽量只有一个人在改。
  4. 看懂 BASE:冲突难以裁决时,先 git show <merge-base> 看一眼原始版本,常常豁然开朗。

小结

三路合并的本质是:用 BASE 区分"谁改了",用 LOCAL/REMOTE 表达"改成了什么"。记住 merge 与 rebase 下 LOCAL/REMOTE 会互换,下次面对 mergetool 的三个窗口就不会再迷糊了。