本节摘要:
git merge把一条分支的改动集成到当前分支,是分支价值的兑现点。本节讲清两种合并策略——快进合并(无新提交,历史线性)与三方合并(找共同祖先,产生合并提交),再介绍 --no-ff、--squash、--no-commit、--abort 等选项,最后给出合并的最佳实践。
阅读完本节,你应当能够:
分支的价值在"开",更在"合"。一条功能分支做完了,不合并回主干,它永远是孤岛——代码不生效、队友看不到、发布不包含。合并就是把"并行的想法"变回"统一的产品"。
但合并不是无脑接水管。两条分支在各自的生命周期里都向前走了,怎么把两边的改动拼回一个正确的整体?Git 的答案基于一个巧妙的观察:只要找到两条分支"分道扬镳"的那个共同祖先,就能理解两边各自新增了什么,再把两个方向的改动合成一个。这个"找共同祖先再三方对比"的思路,就是整个合并机制的核心。
合并还牵扯一个历史观的问题:你希望合并后的历史是"如实记录分叉与汇合",还是"假装一直是一条直线"? Git 给两种选择——merge 保留真实(有合并提交),rebase 伪造直线(下一节讲)。本节先把 merge 这条"真实路线"讲透。
需要先说清一点:合并不是一个"可能失败的附加步骤",而是 Git 工作流里绕不开的日常动作。只要团队超过两个人、分支超过两条,合并就以分钟级的频率发生。因此"会合并"和"会解决冲突"几乎是 Git 入门的及格线,而"合并前想清楚方向、合并后验证结果"则是区分熟练与生手的标尺。本节的篇幅配得上它的重要性——把机制、选项、实践一次讲全,避免你以后边用边猜。
git merge 分支B 把分支B 合入当前所在分支。所以合并前必须先切到"接收方":
git switch main # 到接收方 git merge feature # 把 feature 合进 main
方向别搞反——把 main 合进 feature 是另一种需求(同步主干到功能线)。日常"功能完成后合回主干"是第一种。
当接收方分支自"分叉点"以来没有任何新提交时,Git 不需要做任何合并计算——直接把接收方的指针快进到待合并分支的最新提交即可。
特点:不产生合并提交,历史保持线性。代价:合并后看不出"这里是曾经分叉过的 feature 分支合进来的"——分支的上下文信息丢失了。对想保持历史干净的单人项目,快进合并很好用。
当接收方在分叉后也有了自己的新提交,历史真正分叉了。Git 找出两条分支的共同祖先,比较三个点:共同祖先、当前分支最新、待合并分支最新,然后把两边的改动合并。如果成功,生成一个合并提交,它有两个父提交。
合并提交的"两个爸爸"在历史图上形成一个小菱形的交汇点。这正是"历史如实记录分叉与汇合"的形态——看 git log --graph,分支从哪分、在哪合,一目了然。

--no-ff(no fast-forward):即使可以快进,也强制做一次三方合并、产生合并提交。目的是保留分支的上下文——让人能看出"这里曾经有一条功能分支合进来"。适合团队约定、功能较大、或想留个"里程碑节点"的场景。
--squash:把待合并分支的所有改动压成当前分支上的一个改动集合,然后你手动提交一次。效果是"功能的分支历史被丢弃,只留下一个干净的最终状态"。适合"功能分支上一堆 WIP 提交,不想污染主干"的场景。
| 选项 | 产生合并提交 | 保留分支历史 | 适用场景 |
|---|---|---|---|
| 默认(快进可能) | 否 | 否 | 单人、线性历史 |
| --no-ff | 是 | 是 | 团队留上下文 |
| --squash | 手动提交 | 否 | 压缩 WIP |
除了 --no-ff 和 --squash,merge 还有几个值得认识的选项:
--no-commit:执行合并但不自动创建合并提交,把改动留在暂存区。适合你想在合并提交前再补几笔修改,或把多个合并合并成一次提交。完成后手动 git commit 收尾。
--abort:中止合并,回到合并前状态。这是冲突时最重要的退路——git merge --abort 会把工作区和暂存区都还原,仿佛没合并过一样。
--quit:停止合并但保留现场(不还原文件)。调试合并问题时用,平时几乎用不上,知道存在即可。
-m 消息:为合并提交指定消息,替代打开编辑器。git merge feature -m "合并登录功能"。注意 -m 只对会产生合并提交的三方合并生效。
-s 策略:指定合并策略(recursive、ours、theirs、resolve 等)。日常用默认的 recursive 就够了,ours/theirs 在特定场景(如强制保留某一方的文件)才需要,本书不展开。
做一个能亲眼看到"分叉→汇合"的练习:
git init merge-demo && cd merge-demo echo base > shared.txt && git add . && git commit -m "共同祖先" git switch -c feature echo feature > shared.txt && git commit -am "feature 改 shared" git switch main echo main > shared.txt && git commit -am "main 也改 shared" git merge feature # 触发冲突或合并
这个剧本里 main 和 feature 都改了 shared.txt 的同一行——历史分叉了,Git 无法自动判断保留哪个版本,于是报出冲突。这是理解"为什么需要共同祖先"的最佳实验:没有共同祖先的概念,Git 根本不知道两边各自相对于什么做了改动。下一节我们就专门处理冲突。
最后把合并的历史哲学讲透。想象团队两周的协作史:
两种视角没有对错,是"真实记录"与"易读表达"之间的权衡。我的建议很明确:公共分支(多人共享的 main/develop)用 merge,保持历史真实可查;个人私有分支,你自己爱怎么整理都行(rebase 主场)。这个分寸,下一节讲 rebase 时会再次强调。
git merge main,把主干的新改动及时合进来,减少最后合并时的一次性冲突。git switch main git merge feature # 若输出 "Fast-forward" → 快进合并完成 # 若打开编辑器让你写合并提交消息 → 三方合并 git log --graph --oneline --all # 看合并后的历史形态
合并失败 = 冲突(下一节专门讲)。冲突时 merge 会暂停,status 列出冲突文件,等你解决后 add + commit。不想解决了,git merge --abort 可以回到合并前状态——这是一条重要的退路,记住它。
⚠️ 常见坑:在错误的接收分支上执行 merge(比如把 main 合进了 feature 而非相反)。合并前
git status确认当前分支,是最便宜的保险。
💡 关键直觉:把合并想成"两人分别写完同一章节的两半,主编把两半拼起来"。快进合并是"另一个作者根本没写,直接采用你的稿子";三方合并是"两人都写了,主编拿最初大纲对比两边,各取所长,产出合并稿"。
"合并后分支还能继续用吗?" 能。合并只是把 feature 的提交引入了 main,feature 指针还停在自己位置。之后可以继续在 feature 上开发再合并,也可以删除它。
"快进合并是不是永远更好?" 不是。快进虽然历史干净,但丢掉了分支上下文——你无法从历史上看出"这个功能是在独立分支上做的"。团队项目常选 --no-ff 保留这种上下文,方便回溯"这次发布合入了什么"。
"合并提交可以回滚吗?" 可以。用 git revert -m 1 合并提交 回滚整个合并(-m 指定保留哪个父提交),第 5 章会讲 revert。比逐个回滚快进合并后的线性提交更方便——这正是 --no-ff 的一个好处。
"merge 和 pull 有什么关系?" git pull 本质上是"fetch(抓取)+ merge(合并)"的组合:从远程抓取,再合入当前分支。所以第 4 章讲 pull 时,你会再次见到 merge 的身影。
"合并了多次,为什么历史图越来越乱?" 因为每次三方合并都会留下一个带双亲的合并提交,多个分支反复合并就会织出网。想让图清爽,要么限制并行分支的数量,要么对个人私有分支用 rebase(下一节),要么接受"网"本身——它如实反映了并行开发的真实形态,不全是坏事。
"git merge 时提示 'Already up to date' 是什么意思?" 说明你要合并的分支没有当前分支没有的提交——它已经全被合进来了(或根本没有独有提交)。这是正常提示,不是错误,什么都不用做。
"能不能把 main 的改动合进 feature?反过来一样吗?" 方向不同,效果不同。git switch feature && git merge main 是把主干新改动引入功能分支,让功能分支保持与主干同步——这是开发期间的高频操作。git switch main && git merge feature 才是"功能完成后合回主干"。两个方向都合法,别搞混接收方。
"合并时提示 conflicts 但又自动解决了,正常吗?" 正常。Git 只会在"两边改了同一处且无法自动取舍"时停下要你处理;如果两边改的是不同区域,它能自动合并。所以冲突不是合并的常态,自动合并才是——你平时看到的"合并成功"大多是自动完成的。
下一节硬碰硬:合并冲突怎么识别、怎么解决、怎么提交或中止。