第10章:分支合并 —— 殊途同归的艺术
14 分钟阅读
第10章:分支合并 —— 殊途同归的艺术
分支让代码分道扬镳,合并让它们重新交汇。合并是 Git 中最重要也最复杂的操作之一——做得好,代码完美融合;做不好,冲突让人头大。本章教你成为合并大师。
10.1 git merge:合并的艺术
git merge 是 Git 中用于合并分支的命令。它将两个或多个分支的历史整合在一起,创造出新的提交。简单来说,就是把不同分支上的工作成果汇集到一起。
合并的基本概念
当两个分支从共同的起点分道扬镳,各自发展后,你可能希望将它们重新合并。这就像两条河流在下游汇合一样。
想象一下这样的场景:
- 你和同事都从 main 分支的提交 B 开始工作
- 你创建了 feature 分支,做了提交 C 和 D
- 同事直接在 main 分支上做了提交 E
- 现在你想把 feature 分支的工作合并回 main
合并前:
main: A --- B --- E
↑
起点
\
C --- D (feature)
合并后:
main: A --- B --- E --- F
\ /
C --- D (feature)
F 是合并提交,它有两个父提交:E 和 D
什么是合并提交(Merge Commit)?
合并提交是一种特殊的提交,它有两个(或多个)父提交。普通的提交只有一个父提交(除了初始提交没有父提交),而合并提交把两条历史线连接在一起。
基本用法
| |
合并提交的特点
合并提交有两个重要的特性:
- 多父提交:普通提交只有一个父提交,合并提交有两个或多个
- 不修改内容:合并提交本身通常不引入新代码,只是把已有的改动整合
| |
合并时的提交信息
Git 会自动生成合并提交的提交信息,通常是 “Merge branch ‘feature-name’"。你可以修改这个信息:
| |
10.2 快进合并 vs 三方合并:Git 的两种策略
Git 有两种合并策略:快进合并(Fast-forward)和三方合并(Three-way merge)。理解它们的区别,能帮助你更好地控制项目历史。
快进合并(Fast-forward)
当目标分支没有新的提交时,Git 可以直接"快进"指针,不需要创建新的合并提交。
想象一下:你创建 feature 分支后,main 分支没有任何改动。这时候 feature 分支就是 main 分支的"快进"版本。
快进合并前:
A --- B (main)
\
C --- D (feature)
快进合并后:
A --- B --- C --- D (main, feature)
main 指针直接移动到 D,没有创建新的合并提交
整个历史看起来就是一条直线
| |
快进合并的特点:
- 不产生新的合并提交
- 历史记录保持线性,看起来就像直接在 main 上开发
- 操作简单,历史干净
快进合并的缺点:
- 看不出曾经用过 feature 分支
- 如果 feature 分支有很多小提交,main 历史会变得很乱
三方合并(Three-way merge)
当两个分支都有新的提交时,Git 需要创建一个新的合并提交来整合两条历史线。
三方合并前:
main: A --- B --- C
↑
共同祖先
\
D --- E (feature)
三方合并后:
main: A --- B --- C --- F
\ /
D --- E (feature)
F 是新的合并提交,有两个父提交:C 和 E
| |
三方合并的特点:
- 创建新的合并提交
- 保留完整的分支历史
- 清楚地显示分支合并点
三方合并的缺点:
- 历史图变得复杂(有分叉和合并)
- 合并提交可能被认为"污染"历史
选择合并策略
| 策略 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| Fast-forward | 简单功能开发、个人项目 | 历史简洁、没有多余的合并提交 | 看不出曾用分支 |
| Three-way | 复杂功能开发、团队协作 | 保留完整历史、清晰显示合并点 | 历史图复杂 |
建议:
- 个人项目或简单改动:使用快进合并
- 团队协作或复杂功能:使用三方合并,保留历史
10.3 禁止快进合并:--no-ff 保留分支历史
有时候,即使可以快进合并,你也想保留分支信息。这时候 --no-ff 参数就派上用场了。
为什么禁止快进?
快进合并虽然简洁,但会丢失分支信息:
快进合并后:
A --- B --- C --- D --- E --- F (main)
看起来就像直接在 main 上开发的,
根本不知道曾经有个 feature 分支,
也看不出什么时候合并的
禁止快进合并后:
A --- B --------- F (main)
\ /
C --- D (feature)
清楚地看到:
1. feature 分支的存在
2. 从 B 点开始开发
3. 在 F 点合并回 main
使用 --no-ff
| |
配置默认禁止快进
如果你希望默认禁止快进合并,可以配置 Git:
| |
何时使用 --no-ff
推荐使用 --no-ff 的场景:
- 功能分支合并到 main:保留功能开发的历史
- 发布分支合并:标记发布点
- 任何你想保留分支历史的场景
不推荐使用 --no-ff 的场景:
- 个人临时分支:没必要保留
- 简单的快速修复:保持历史简洁
- 历史已经够复杂了:不要再添加合并提交
实际案例
假设你开发了一个用户登录功能:
| |
10.4 压缩合并:--squash 把多个提交变成一个
有时候,功能分支有很多"半成品"提交,合并到 main 时你想把它们压缩成一个干净的提交。这就是 --squash 的用武之地。
什么是 Squash 合并?
Squash 合并会把分支上的所有提交"压缩"成一个提交,然后合并到目标分支。
想象一下:你在 feature 分支上开发了 5 天,每天都有提交:
feature 分支历史:
A --- B --- C (main)
\
D --- E --- F --- G --- H (feature)
↑ ↑ ↑ ↑ ↑
开始 WIP 修复 优化 完成
这些提交信息可能是:
- D: "开始开发"
- E: "WIP: 添加登录表单"
- F: "修复表单验证"
- G: "优化样式"
- H: "完成登录功能"
Squash 合并后:
main: A --- B --- C --- I
\ /
D --- E --- F --- G --- H (feature,可删除)
I 是一个提交,包含了 D、E、F、G、H 的所有改动
提交信息可能是:"feat: add user login functionality"
feature 分支的历史被"压缩"了,main 分支只有一个干净的提交
使用 --squash
| |
Squash 合并的特点
- 不产生合并提交:历史保持线性
- 丢失分支历史:feature 分支的提交历史不会保留在 main
- 改动合并:所有改动压缩成一个提交
- 需要手动提交:Git 不会自动创建提交,让你有机会写好的提交信息
Squash 合并 vs 普通合并
| 特性 | 普通合并 | Squash 合并 |
|---|---|---|
| 合并提交 | 有 | 无 |
| 分支历史 | 保留 | 丢失 |
| 提交数量 | 多个 | 一个 |
| 历史图 | 有分叉 | 线性 |
何时使用 Squash
推荐使用 Squash:
- 功能分支有很多"WIP”(进行中)提交
- 想保持 main 分支历史简洁
- 功能开发过程中提交很乱(“fix”、“update”、“WIP”)
- 不想暴露开发过程中的试错历史
不推荐使用 Squash:
- 想保留详细的开发历史
- 需要回溯功能开发过程
- 团队协作需要完整历史(调试时需要)
- 法规要求保留完整开发记录
实际案例
假设你开发一个功能,过程中有很多小提交:
| |
10.5 合并冲突:当两个世界碰撞
合并冲突是 Git 中最让人头疼的问题。当两个分支修改了同一个文件的同一部分,Git 无法自动决定用哪个版本,就会产生冲突。
冲突是如何产生的?
想象这样一个场景:
main 分支修改了 file.txt 的第 10 行:
A --- B --- C (main)
修改第10行:console.log("Hello, World!");
feature 分支也修改了 file.txt 的第 10 行:
A --- B --- D (feature)
修改第10行:console.log("Hi, Universe!");
合并时:
Git:"第10行到底用哪个版本?
main 分支说是 'Hello, World!',
feature 分支说是 'Hi, Universe!',
我不知道该听谁的!"
结果:冲突!
冲突的标志
当发生冲突时,Git 会明确告诉你:
| |
冲突的状态
发生冲突后,Git 会进入"未合并"状态:
| |
状态解读:
- “Unmerged paths”:有未合并的文件
- “both modified: file.txt”:file.txt 在两个分支都被修改了
- 需要解决冲突后执行
git add标记为已解决
10.6 冲突标记解读:<<<<<<< HEAD 是什么鬼?
冲突标记看起来吓人,其实很好理解。一旦掌握规律,解决冲突就是小菜一碟。
冲突标记的结构
当 Git 无法自动合并时,它会在文件中插入特殊的标记,把冲突内容展示给你:
<<<<<<< HEAD
当前分支(你所在分支)的内容
=======
被合并分支的内容
>>>>>>> branch-name
各部分含义:
<<<<<<< HEAD:当前分支内容的开始标记=======:分隔线,上面是当前分支,下面是对方分支>>>>>>> branch-name:对方分支内容的结束标记
实际例子
假设你在合并 feature 分支时发生冲突:
| |
解读:
<<<<<<< HEAD到=======之间:是 main 分支的内容=======到>>>>>>> feature之间:是 feature 分支的内容- 你需要决定保留哪个,或者合并两个版本
解决冲突的步骤
- 打开冲突文件,找到冲突标记
- 决定保留哪个版本,或者合并两个版本
- 删除冲突标记(
<<<<<<<、=======、>>>>>>>) - 保存文件
- 标记为已解决:
git add file.js - 完成合并:
git commit
解决后的文件
| |
10.7 手动解决冲突:三种方式任你选
解决冲突有三种基本策略:保留当前、保留对方、合并双方。选择哪种取决于具体情况。
方式1:保留当前分支的版本
如果你确定当前分支的版本是正确的:
冲突前:
<<<<<<< HEAD
当前版本
=======
对方版本
>>>>>>> feature
解决后(保留当前):
当前版本
| |
适用场景:
- 你确定当前分支的版本是正确的
- 对方的改动是过时的
- 对方的改动与当前改动冲突,但当前改动更重要
方式2:保留对方分支的版本
如果你确定对方分支的版本更好:
冲突前:
<<<<<<< HEAD
当前版本
=======
对方版本
>>>>>>> feature
解决后(保留对方):
对方版本
| |
适用场景:
- 对方分支的版本是更新的
- 当前分支的版本是过时的
- 对方修复了某个问题,你没有
方式3:合并两个版本
有时候,两个版本都有价值,需要合并:
冲突前:
<<<<<<< HEAD
function calculate(a, b) {
return a + b;
}
=======
function calculate(x, y) {
return x * y;
}
>>>>>>> feature
解决后(合并两个功能):
function calculate(a, b, operation = 'add') {
if (operation === 'add') {
return a + b;
} else if (operation === 'multiply') {
return a * b;
}
}
适用场景:
- 两个版本都实现了有用的功能
- 可以整合两个版本的优点
- 删除任何一个都会导致功能丢失
批量解决冲突
如果有多个文件的冲突,你想统一处理:
| |
10.8 VS Code 冲突解决:图形化真香
手动编辑冲突文件容易出错,VS Code 提供了图形化的冲突解决工具,让解决冲突变得轻松愉快。
VS Code 的冲突界面
当打开冲突文件时,VS Code 会显示友好的界面:
<<<<<<< HEAD (Current Change)
当前分支的内容
=======
Incoming Change
对方分支的内容
>>>>>>> feature
[Accept Current Change] [Accept Incoming Change] [Accept Both Changes] [Compare Changes]
操作按钮
- Accept Current Change:保留当前分支的版本
- Accept Incoming Change:保留对方分支的版本
- Accept Both Changes:保留两个版本(当前在前,对方在后)
- Compare Changes:对比两个版本的差异
使用步骤
- 打开冲突文件
- 点击相应的按钮选择解决方案
- 保存文件
- 在终端执行
git add . - 执行
git commit完成合并
其他图形化工具
| 工具 | 平台 | 特点 |
|---|---|---|
| VS Code | 跨平台 | 内置,免费,功能强大 |
| SourceTree | Win/Mac | 可视化分支图,操作简单 |
| GitKraken | 跨平台 | 漂亮的界面,功能丰富 |
| IntelliJ IDEA | 跨平台 | 强大的 IDE 集成 |
| Beyond Compare | Win/Mac | 专业的对比工具,功能强大 |
10.9 合并最佳实践:避免冲突的秘诀
冲突虽然可以解决,但最好避免。以下是一些最佳实践,帮助你减少冲突的发生。
1. 频繁同步
| |
为什么有效:
- 减少与主分支的差异
- 及早发现和解决冲突
- 避免最后合并时的"大爆炸"
2. 小步提交
| |
3. 分工明确
| |
4. 使用特性开关(Feature Flag)
| |
5. 及时删除已合并分支
| |
6. 代码审查
| |
10.10 合并后后悔了?怎么撤销?
合并后发现有问题?别担心,可以撤销。
场景1:还没 push
| |
场景2:已经 push
| |
场景3:想重新合并
| |
场景4:合并过程中想放弃
| |
本章小结
本章我们学习了分支合并的各种知识:
| 主题 | 要点 |
|---|---|
git merge | 合并分支的基本命令 |
| 快进合并 | 不产生合并提交,历史线性 |
| 三方合并 | 创建合并提交,保留分支历史 |
--no-ff | 强制创建合并提交 |
--squash | 压缩多个提交为一个 |
| 冲突解决 | 手动编辑或使用图形化工具 |
| 最佳实践 | 频繁同步、小步提交、分工明确 |
关键要点:
- 快进合适合简单场景,三方合并保留完整历史
--no-ff可以强制保留分支信息--squash可以保持主分支历史简洁- 冲突不可怕,理解冲突标记的结构就能解决
- 预防胜于治疗:频繁同步、小步提交
记忆口诀:
- 简单合并 → 快进
- 保留历史 →
--no-ff - 历史简洁 →
--squash - 有冲突 → 找
<<<<<<<
练习建议:
- 创建两个分支,故意制造冲突并解决
- 尝试快进合并、三方合并、Squash 合并
- 使用 VS Code 的冲突解决工具
下一章,我们将进入远程仓库的世界!