- Published on
搞懂 Git 的三路合并
- Authors

- Name
- Tails Azimuth
从一个冲突说起
合并代码时打开冲突文件,你会看到这样的标记:
<<<<<<< HEAD
当前分支的内容
=======
被合并分支的内容
>>>>>>> feature
但 Git 凭什么判断这是冲突、又凭什么能自动合并另一部分?答案藏在三个文件版本里:BASE、LOCAL、REMOTE。
三个版本
BASE —— 共同祖先
两个分支分叉之前那一刻,文件的内容。它是合并的参考基准:只有相对 BASE 发生了变化,才算一次"修改"。
LOCAL —— 当前分支
你现在所在分支上的文件内容。也就是 HEAD。
REMOTE —— 被合并方
即将合并进来的那个分支上的文件内容。
三路判定的逻辑
Git 拿这三个版本做两次比较:
BASEvsLOCAL→ 你改了什么;BASEvsREMOTE→ 对方改了什么。
然后分情况:
| 你的修改 | 对方的修改 | 结果 |
|---|---|---|
| 改了 A 行 | 没改 A 行 | 采纳你的 |
| 没改 A 行 | 改了 A 行 | 采纳对方的 |
| 改了 A 行 | 改了 A 行(不同) | 冲突,需人工裁决 |
| 都没改 | 都没改 | 保持 BASE |
关键在于:合并器并不直接拿 LOCAL 和 REMOTE 硬碰硬,而是借助 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。合并 feature 到 main 时,Git 比较的是 B→C 与 B→E 这两条变化路径。
用 mergetool 处理冲突
git mergetool
它会为每个冲突文件准备四个版本:
file.LOCAL # 当前分支版本
file.REMOTE # 要合并进来的版本
file.BASE # 共同祖先版本
file # 你要写入的合并结果
打开三方合并工具(如 vimdiff、meld)时,同时看到 LOCAL、BASE、REMOTE 三个窗口会大有帮助——BASE 能告诉你"原本是什么样",从而判断两侧各自的改动意图。
减少冲突的几条实践
- 小步快跑:提交粒度小,冲突范围也小。
- 频繁同步:长期分叉的分支,合并时几乎必然大面积冲突;定期
merge/rebase主干。 - 约定改谁:对同一模块,同一时间尽量只有一个人在改。
- 看懂 BASE:冲突难以裁决时,先
git show <merge-base>看一眼原始版本,常常豁然开朗。
小结
三路合并的本质是:用 BASE 区分"谁改了",用 LOCAL/REMOTE 表达"改成了什么"。记住 merge 与 rebase 下 LOCAL/REMOTE 会互换,下次面对 mergetool 的三个窗口就不会再迷糊了。