Git 合并策略深度解析:recursive、ort、octopus 等底层合并算法的原理与实战选择
在日常使用 Git 的过程中,大多数开发者对
1 | git merge |
命令再熟悉不过了。但当我们执行
1 | git merge feature-branch |
时,Git 究竟在背后做了什么?为什么有时候合并会触发冲突,而有时候却丝滑无感?为什么有的仓库推荐用
1 | ort |
策略,而有的项目必须用
1 | octopus |
?
Git 的合并并非简单的”把两个分支的代码拼在一起”——它内置了多种合并策略(merge strategies),每种策略适用于不同的场景。理解这些策略的底层原理,不仅能帮助你在合并冲突时做出更明智的决策,还能让你在多人协作的大型项目中游刃有余。本文将深入剖析 Git 的各大合并策略,从经典的 recursive 到新一代的 ort,再到用于多分支合并的 octopus,带你掌握 Git 合并的”内功心法”。

一、Git 合并策略概览
Git 的合并策略(merge strategy)是决定如何将两个或多个分支的修改合并到一起的底层算法。自 Git 诞生以来,一共引入了以下几种主要策略:
| 策略名称 | 适用场景 | 多分支支持 | 驱动方式 | ||
|---|---|---|---|---|---|
|
两个分支的合并(默认) | 否 | 递归三路合并 | ||
|
两个分支的合并(新一代) | 否 | 重写的三路合并 | ||
|
两个分支的合并(旧式) | 否 | 二路合并 | ||
|
三个及以上分支的合并 | 是 | 多路合并 | ||
|
完全保留当前分支内容 | 是 | 忽略合并内容 | ||
|
子树合并场景 | 否 | 特殊递归 |
在 Git 2.33 之前,
1 | recursive |
是默认的合并策略。而从 Git 2.33 开始,
1 | ort |
(Ostensibly Recursive’s Twin)成为了新的默认策略。了解它们的区别,对于理解现代 Git 的行为至关重要。
二、recursive 策略:经典的递归三路合并
2.1 基本原理
recursive 策略的核心思想是”三路合并”(Three-way Merge)。三路合并需要三个”快照”:
- Base:两个分支最近的共同祖先提交
- Ours:当前分支的最新提交
- Theirs:待合并分支的最新提交
Git 对比 Base→Ours 和 Base→Theirs 的差异,如果同一个文件在同一位置只被一方修改过,合并自然成功;如果双方都修改了同一位置,则产生冲突。
2.2 递归场景
recursive 策略之所以叫”递归”,是因为它处理了”共同祖先不唯一”的情况。当两个分支之间有过多次合并历史时,可能会产生多个共同祖先。此时 recursive 策略会创建一个”虚拟的”共同祖先(virtual merge base):
1
2
3
4 # 示意:虚拟祖先的创建过程
# 假设 A 和 B 有两个共同祖先 C1 和 C2
# recursive 策略先将 C1 和 C2 合并为虚拟祖先 V
# 再将 A 和 B 以 V 为 base 进行合并
这种递归合并的方式确保了即使分支历史复杂,也能找到最合理的合并基础。recursive 策略还支持多种子策略(sub-strategies)来优化合并行为:
-
1patience
:使用”耐心算法”进行差异对比,适合代码重构后的大范围移动
-
1minimal
:最小化差异,尽可能生成更精确的差异
-
1histogram
:使用直方图差异算法,是 patience 的改进版(Git 默认推荐)
-
1no-renames
:不检测文件重命名,适用于某些特殊场景
-
1find-renames=<n>
:设置重命名检测的相似度阈值
2.3 实战:使用 recursive 子策略
1
2
3
4
5
6
7
8
9
10
11 # 使用 patience 算法进行合并,适合代码大规模重构场景
git merge -s recursive -X patience feature-branch
# 使用 histogram 算法(推荐,Git 默认)
git merge -s recursive -X histogram feature-branch
# 关闭重命名检测
git merge -s recursive -X no-renames feature-branch
# 设置重命名检测阈值(相似度 80% 以上才被视为重命名)
git merge -s recursive -X find-renames=80 feature-branch
三、ort 策略:新一代合并引擎
3.1 为什么需要 ort?
recursive 策略虽然可靠,但有一些长期存在的问题:
- 性能瓶颈:recursive 需要将整个索引结构反复写入磁盘,在大仓库中速度很慢
- 内存浪费:每次递归调用都要重新加载索引,导致大量 I/O 开销
- 冲突处理能力有限:在某些复杂合并场景下,recursive 会不必要地标记冲突
- 代码维护困难:recursive 的代码经过多年迭代,变得非常复杂
ort(Ostensibly Recursive’s Twin)由 Elijah Newren 在 2020 年开始重写,目标是在不改变合并结果的前提下,全面优化性能、减少冲突误报、并让代码更易于维护。ort 从 Git 2.33 开始成为默认合并策略。
3.2 ort 的核心改进
ort 与 recursive 最根本的区别在于数据流架构:
| 特性 | recursive | ort |
|---|---|---|
| 数据存储 | 频繁写入磁盘索引 | 全程在内存中处理 |
| 合并速度 | 中等(大仓库数分钟) | 快(大仓库秒级) |
| 冲突处理 | 粗粒度,易误报 | 细粒度,精确识别 |
| 重命名检测 | 基础支持 | 增强支持(目录重命名、跨文件重命名) |
| 文件冲突判决 | 有限 | 更智能的判决(如 MODIFY/DELETE) |
一个典型的性能对比:在 Linux 内核仓库(200 万+提交)中合并两个分支,recursive 需要约 120 秒,而 ort 只需要 15 秒左右,性能提升约 8 倍。
3.3 ort 的”不冲突”优化
ort 在冲突检测方面做了大量优化。例如,在以下场景中 recursive 会报冲突,但 ort 可以自动合并:
1
2
3
4
5
6
7 # 场景:文件 A 在 branch1 中被删除,在 branch2 中被修改
# recursive 策略:标记为 CONFLICT (modify/delete)
# ort 策略:自动选择删除(前提是删除方有合理理由)
# 场景:文件被重命名,同时内容被修改
# recursive 策略:有时会标记为冲突
# ort 策略:自动跟踪重命名 + 合并修改
四、octopus 策略:多分支合并的艺术
4.1 什么是 octopus 合并?
octopus 策略用于同时合并三个或更多分支。它的名字来源于章鱼(octopus)的八条触手,形象地表示它能同时”抓住”多个分支。在 Git 中,可以通过以下命令触发 octopus 合并:
1
2
3
4
5 # 同时合并三个功能分支
git merge feature-a feature-b feature-c
# 显式指定 octopus 策略
git merge -s octopus feature-a feature-b feature-c
4.2 octopus 的工作原理
octopus 策略的核心算法是”串行多路合并”:
- 以当前分支为起点
- 依次将每个待合并分支作为”theirs”进行合并
- 如果任何一步产生冲突,立即中止整个合并过程
这意味着 octopus 策略要求所有待合并分支之间不能有冲突——它不会像 recursive 那样处理复杂的冲突解决。如果 A 分支和 B 分支修改了同一个文件的同一行,octopus 合并会直接失败。
4.3 octopus 的适用场景
octopus 策略最适合以下场景:
- 主题分支汇总:多个独立的功能分支同时合并到 develop 或 main 分支
- 修复合并:多个 hotfix 分支同时合并到 release 分支
- CI/CD 自动化:在自动化脚本中批量合并无冲突的变更
1
2
3
4
5
6
7 # 实际项目中的 octopus 合并示例
# 假设有三个独立的功能分支
git checkout develop
git merge -s octopus feature/login feature/search feature/admin
# 如果成功,Git 会生成一个"章鱼提交"(octopus merge commit)
# 这个提交有 4 个父提交(develop + 3 个 feature)
4.4 octopus 的局限性
尽管 octopus 看起来很强大,但它有严格的限制:
- 零冲突容忍:任何分支间的冲突都会导致合并失败
- 无子策略:octopus 不支持
1-X
参数(如
1-X patience)
- 可读性争议:章鱼提交的父提交过多,在图形化历史中难以阅读
- 回滚复杂:如果章鱼提交出现问题,revert 操作会变得非常复杂
因此,在实际项目中,octopus 的使用频率远低于 recursive/ort。大多数团队更倾向于逐个合并分支,保持历史清晰可读。
五、ours 策略与 subtree 策略
5.1 ours 策略:完全保留当前分支
ours 策略虽然名字叫”ours”,但它并不是”只取我们的修改”那么简单。它的行为是:记录合并历史,但完全忽略待合并分支的内容变更,最终结果与当前分支完全一致。
1
2
3
4
5
6 # 使用 ours 策略合并
git merge -s ours feature-branch
# 这会在 Git 历史中留下一个合并提交
# 表明 "feature-branch 曾经被合并过"
# 但实际上 feature-branch 的代码变更没有被应用
ours 策略的典型用途:
- 标记已废弃的分支:当某个功能分支被废弃,但希望在历史中保留其存在的记录
- 绕过冲突:临时合并一个分支但不引入其变更,以便后续手动合并
- 重新组织分支拓扑:在重构分支结构时保持历史整洁
5.2 subtree 策略:优雅的子树合并
subtree 策略用于将一个子项目合并到当前仓库的子目录中。它类似于 submodule,但代码是直接存储在仓库中的。
1
2
3
4
5
6
7
8 # 将外部项目作为子树合并到指定目录
git subtree add --prefix=lib/foo https://github.com/example/foo.git main --squash
# 使用 subtree 策略进行合并
git merge -s subtree external-branch
# 从子树推送更新到上游
git subtree push --prefix=lib/foo https://github.com/example/foo.git main
subtree 与 submodule 的对比:
| 特性 | subtree | submodule | ||
|---|---|---|---|---|
| 代码存储 | 直接在主仓库中 | 独立的子仓库引用 | ||
| 克隆体验 | 一次克隆即可 | 需要
|
||
| 历史重写 | 会污染主仓库历史 | 独立历史 | ||
| 修改复杂度 | 需要专门工具 | 可以直接在子目录修改 | ||
| 适用场景 | 小工具、依赖库 | 大型子项目 |
六、如何查看和选择合并策略
6.1 查看当前仓库的合并策略配置
1
2
3
4
5
6
7
8
9
10
11 # 查看全局合并策略配置
git config --global merge.default
# 查看特定仓库的配置
git config merge.default
# 查看 Git 支持的合并策略列表
git merge -s help
# 查看当前合并策略的版本
git merge -s recursive -v --help
6.2 策略选择的实战建议
根据不同的项目规模和团队协作方式,以下是推荐的策略选择方案:
- 小型项目(< 50 人团队):使用默认的 ort 策略即可,无需额外配置
- 中型项目(50-200 人团队):开启
1merge.renormalize
和
1merge.conflictStyle为
1diff3,提高冲突可读性:
1
2 git config --global merge.conflictStyle diff3
git config --global merge.renormalize true
- 大型仓库(如 Linux 内核、Android AOSP):强烈建议使用 Git 2.33+ 以确保 ort 策略生效,并配置
1merge.verbosity
控制日志输出:
1
2
3
4
5 # 减少合并时的详细输出
git config --global merge.verbosity 0
# 遇到冲突时自动创建备份
git config --global merge.backup true
- 多分支并行开发:如果经常需要合并 3 个以上无冲突的分支,可以配置 octopus 为默认策略;否则建议逐个合并。
七、常见的合并陷阱与解决方案
7.1 策略选择错误导致合并失败
1
2
3
4
5
6
7
8
9 # 错误:对三个分支使用了默认的 recursive 策略
# 这会导致 recursive 报错,因为它只支持两个分支
# 正确做法:显式指定 octopus 策略
git merge -s octopus feature-a feature-b feature-c
# 或者:逐个合并
git merge feature-a
git merge feature-b
git merge feature-c
7.2 递归策略中的”修改/删除”冲突
1
2
3
4
5
6
7 # 当一方删除了文件,另一方修改了文件时
# 可以通过 -X 参数指定策略行为
# 倾向于接受修改
git merge -s recursive -X theirs feature-branch
# 倾向于保留删除
git merge -s recursive -X ours feature-branch
7.3 合并策略与 .gitattributes 的交互
1 | .gitattributes |
文件中的合并设置会影响策略的行为。例如,可以为特定文件类型指定自定义合并驱动:
1
2
3
4
5
6
7
8 # .gitattributes 文件示例
# 对 .lock 文件(如 Unity 场景文件)使用 union 合并策略
*.unity use-union-merge
*.lock merge=union
# 对二进制文件始终使用 ours
*.png merge=ours
*.jpg merge=ours
八、总结
Git 的合并策略体系是一个经过数十年演进的精妙系统。从经典的 recursive 到新一代的 ort,从多分支的 octopus 到特殊用途的 ours 和 subtree,每种策略都针对特定的场景做了优化。理解它们的底层原理,可以帮助你在实际开发中做出更明智的决策,避免”合并恐慌”。
对于绝大多数开发者来说,使用默认的 ort 策略是最佳选择。它性能更好、冲突处理更智能,且完全兼容 recursive 的历史行为。当遇到多分支合并时,可以根据分支间的依赖关系选择 octopus 或逐个合并。对于特殊场景(如子树集成、废弃分支标记),ours 和 subtree 策略也是值得掌握的实用工具。
最后,不要忘记 Git 的合并策略可以通过
1 | .gitattributes |
和
1 | git config |
进行精细调优。花时间理解并配置你的合并环境,将在长期的项目协作中带来可观的效率提升。
希望这篇文章能帮助你从”会合并”进阶到”懂合并”。下次遇到复杂的合并场景时,不妨试试本文提到的策略选项,让你的 Git 使用体验再上一个台阶。
(题图来源:Unsplash)
汤不热吧