<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Merge on 巴巴变的博客</title><link>https://blog.bugxch.top/tags/merge/</link><description>Recent content in Merge on 巴巴变的博客</description><generator>Hugo -- gohugo.io</generator><language>zh-cn</language><lastBuildDate>Sat, 12 Sep 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://blog.bugxch.top/tags/merge/index.xml" rel="self" type="application/rss+xml"/><item><title>Git merge 与 rebase 的本质区别</title><link>https://blog.bugxch.top/p/git-merge-%E4%B8%8E-rebase-%E7%9A%84%E6%9C%AC%E8%B4%A8%E5%8C%BA%E5%88%AB/</link><pubDate>Sat, 12 Sep 2026 00:00:00 +0000</pubDate><guid>https://blog.bugxch.top/p/git-merge-%E4%B8%8E-rebase-%E7%9A%84%E6%9C%AC%E8%B4%A8%E5%8C%BA%E5%88%AB/</guid><description>&lt;img src="https://image-1258996033.cos.ap-shanghai.myqcloud.com/westlake" alt="Featured image of post Git merge 与 rebase 的本质区别" /&gt;&lt;p&gt;&lt;code&gt;git merge&lt;/code&gt; 和 &lt;code&gt;git rebase&lt;/code&gt; 都是把两个分支的成果合到一起的方法，但它们的&lt;strong&gt;历史产出完全不同&lt;/strong&gt;——一个保留分叉结构，一个把历史拉直。很多人知道命令怎么用，却不清楚二者动的&amp;quot;对象&amp;quot;方向其实是反的。&lt;/p&gt;&#10;&lt;p&gt;这篇文章用一个干净的实验讲透：同一分叉状态，&lt;code&gt;merge&lt;/code&gt; 和 &lt;code&gt;rebase&lt;/code&gt; 各自产生什么，本质区别在哪，以及什么时候该用哪个。&lt;/p&gt;&#10;&lt;h1 id="起点一个干净的分叉"&gt;起点：一个干净的分叉&#10;&lt;/h1&gt;&lt;p&gt;假设两个分支 A 和 B 从共同祖先 C0 分叉：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;&#10;&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1&#10;&lt;/span&gt;&lt;span class="lnt"&gt;2&#10;&lt;/span&gt;&lt;span class="lnt"&gt;3&#10;&lt;/span&gt;&lt;span class="lnt"&gt;4&#10;&lt;/span&gt;&lt;span class="lnt"&gt;5&#10;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#10;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;* bd3c398 (A) A1: add a&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;| * 3412a0c (B) B2: add b2&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;| * 96eeddc B1: add b1&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;|/&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;* bf6c99c (main) C0: base&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;/div&gt;&lt;p&gt;A 有一个提交 A1，B 有两个提交 B1、B2，二者互不包含对方。这是 merge 和 rebase 的典型使用场景。&lt;/p&gt;&#10;&#10; &lt;blockquote&gt;&#10; &lt;p&gt;注意一个实验细节：要构造&amp;quot;真分叉&amp;quot;，必须用 &lt;code&gt;git branch A &amp;amp;&amp;amp; git branch B&lt;/code&gt;（都指向 C0）再从各自 checkout 提交。如果直接用 &lt;code&gt;git checkout -b B C0&lt;/code&gt;，B 会包含 A 的提交，A 成为 B 的祖先，merge 会直接 fast-forward，无法演示分叉合并。&lt;/p&gt;&#10;&#10; &lt;/blockquote&gt;&#10;&lt;h1 id="同一状态下的两种操作"&gt;同一状态下的两种操作&#10;&lt;/h1&gt;&lt;p&gt;对相同的分叉状态，克隆两份副本，一份做 &lt;code&gt;git merge B&lt;/code&gt;，一份做 &lt;code&gt;git rebase B&lt;/code&gt;（都在 A 分支上执行）。&lt;/p&gt;&#10;&lt;h2 id="git-merge-b保留分叉新增汇合点"&gt;git merge B：保留分叉，新增汇合点&#10;&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;&#10;&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1&#10;&lt;/span&gt;&lt;span class="lnt"&gt;2&#10;&lt;/span&gt;&lt;span class="lnt"&gt;3&#10;&lt;/span&gt;&lt;span class="lnt"&gt;4&#10;&lt;/span&gt;&lt;span class="lnt"&gt;5&#10;&lt;/span&gt;&lt;span class="lnt"&gt;6&#10;&lt;/span&gt;&lt;span class="lnt"&gt;7&#10;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#10;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;* 6dd707a (A) Merge branch &amp;#39;B&amp;#39; into A ← merge commit，2 个 parent&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;|\&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;| * 3412a0c B2: add b2&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;| * 96eeddc B1: add b1&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;* | bd3c398 A1: add a&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;|/&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;* bf6c99c C0: base&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;/div&gt;&lt;p&gt;merge 的特征：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;产生一个 &lt;strong&gt;merge commit&lt;/strong&gt;（&lt;code&gt;6dd707a&lt;/code&gt;），它有 &lt;strong&gt;2 个 parent&lt;/strong&gt;&lt;/li&gt;&#10;&lt;li&gt;A 和 B 的原始提交 SHA &lt;strong&gt;都不变&lt;/strong&gt;（&lt;code&gt;bd3c398&lt;/code&gt;、&lt;code&gt;3412a0c&lt;/code&gt; 原样保留）&lt;/li&gt;&#10;&lt;li&gt;历史&lt;strong&gt;保留分叉结构&lt;/strong&gt;（两条线 + 一个汇合点）&lt;/li&gt;&#10;&lt;li&gt;A 的内容 = A1 + B1 + B2&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="git-rebase-b拉直历史重放提交"&gt;git rebase B：拉直历史，重放提交&#10;&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;&#10;&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1&#10;&lt;/span&gt;&lt;span class="lnt"&gt;2&#10;&lt;/span&gt;&lt;span class="lnt"&gt;3&#10;&lt;/span&gt;&lt;span class="lnt"&gt;4&#10;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#10;&lt;td class="lntd"&gt;&#10;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;* 3faa28a (A) A1: add a ← A1 被重放，SHA 变了！&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;* 3412a0c (B) B2: add b2 ← B 原样（是 A 的新基底）&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;* 96eeddc B1: add b1&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;* bf6c99c C0: base&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;/div&gt;&lt;p&gt;rebase 的特征：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;没有 merge commit&lt;/strong&gt;&lt;/li&gt;&#10;&lt;li&gt;A 的提交被&lt;strong&gt;重放到 B 之上&lt;/strong&gt;：A1 变成 &lt;code&gt;3faa28a&lt;/code&gt;（新 SHA，parent 从 C0 变成了 B2）&lt;/li&gt;&#10;&lt;li&gt;B 的提交&lt;strong&gt;原样保留&lt;/strong&gt;（&lt;code&gt;3412a0c&lt;/code&gt;、&lt;code&gt;96eeddc&lt;/code&gt; 不变，它们成为新基底）&lt;/li&gt;&#10;&lt;li&gt;历史变成&lt;strong&gt;一条直线&lt;/strong&gt;，分叉消失&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h1 id="本质区别"&gt;本质区别&#10;&lt;/h1&gt;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;维度&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;&lt;code&gt;git merge B&lt;/code&gt;&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;&lt;code&gt;git rebase B&lt;/code&gt;&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;新增提交&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;merge commit（2 parent）&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;无，纯重放&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;A 的提交&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;原样保留，SHA 不变&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;重放，SHA 全变&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;B 的提交&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;原样保留&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;原样保留（成为新基底）&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;历史形态&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;保留分叉（两条线汇合）&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;拉直成一条线&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;结果指向&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;A 指向 merge commit&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;A 指向重放后新 tip&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&#10; &lt;blockquote&gt;&#10; &lt;p&gt;&lt;strong&gt;最容易搞反的一点：二者动的对象不同&lt;/strong&gt;&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;code&gt;git merge B&lt;/code&gt; = &lt;strong&gt;把 B 并入 A&lt;/strong&gt;（B 的成果进入 A）&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;git rebase B&lt;/code&gt; = &lt;strong&gt;把 A 重放到 B 之上&lt;/strong&gt;（A 的提交被搬走，parent 变成 B）&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;它们不是&amp;quot;同一件事的两种做法&amp;quot;，而是方向相反：merge 让 B 的提交进 A；rebase 让 A 的提交骑到 B 头上。&lt;/p&gt;&#10;&#10; &lt;/blockquote&gt;&#10;&lt;p&gt;rebase 的本质可以用一句话概括：&lt;strong&gt;replay all commits of current branch on B&lt;/strong&gt;（把当前分支的所有提交在 B 上重放一遍）。动的对象是&lt;strong&gt;当前分支&lt;/strong&gt;，B 只是新基底。merge 则是 bring B&amp;rsquo;s commits into current branch，动的对象是 &lt;strong&gt;B&lt;/strong&gt;。&lt;/p&gt;&#10;&lt;h1 id="为什么-rebase-会改写历史"&gt;为什么 rebase 会改写历史&#10;&lt;/h1&gt;&lt;p&gt;rebase 不是&amp;quot;移动&amp;quot;旧提交，而是&lt;strong&gt;用同样的改动内容生成一批全新提交&lt;/strong&gt;接到新基点上。旧提交变成悬空提交（对象库里还在，但不再有分支指向它）。&lt;/p&gt;&#10;&lt;p&gt;所以：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;A1 从 &lt;code&gt;bd3c398&lt;/code&gt; 变成 &lt;code&gt;3faa28a&lt;/code&gt;——消息没变，但 &lt;strong&gt;SHA、parent、时间戳全变了&lt;/strong&gt;&lt;/li&gt;&#10;&lt;li&gt;被重放提交的 SHA 变了，是因为它&lt;strong&gt;内容里的 parent 字段变了&lt;/strong&gt;（从 C0 变成 B2），而 SHA 由提交全部内容哈希生成&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&#10; &lt;blockquote&gt;&#10; &lt;p&gt;顺带一提：这也解释了为什么&amp;quot;改写历史&amp;quot;有风险。如果 A 分支已经 push 到远端、别人基于它工作过，你 rebase 后就等于把别人脚下的地基抽掉了——他们的历史会和新远端历史分叉，pull 时直接报错。所以 rebase 只对&lt;strong&gt;没出过门&lt;/strong&gt;的提交用。&lt;/p&gt;&#10;&#10; &lt;/blockquote&gt;&#10;&lt;h1 id="什么时候用哪个"&gt;什么时候用哪个&#10;&lt;/h1&gt;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;场景&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;选择&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;想要线性历史、图干净、分支未共享&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;strong&gt;rebase&lt;/strong&gt;（让当前分支建立在最新目标之上）&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;想保留分支结构和汇合点&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;strong&gt;merge&lt;/strong&gt;（不破坏任何历史）&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;分支已 push、别人可能基于它工作&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;strong&gt;merge&lt;/strong&gt;（共享历史不可改写）&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;h1 id="小结"&gt;小结&#10;&lt;/h1&gt;&lt;p&gt;一句话记法：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;code&gt;git merge B&lt;/code&gt; = bring B&amp;rsquo;s commits into current branch（动的是 B）&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;git rebase B&lt;/code&gt; = replay current branch&amp;rsquo;s commits on B（动的是当前分支）&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;merge 保留分叉、产生 merge commit、不改写任何历史；rebase 拉直历史、重放当前分支提交、改写 SHA。理解了&amp;quot;动的对象方向相反&amp;quot;这一点，就不会再把两者混为一谈了。&lt;/p&gt;&#10;&lt;h1 id="参考文献"&gt;参考文献&#10;&lt;/h1&gt;&lt;hr&gt;&#10;&lt;p&gt;本文原载于 &lt;a class="link" href="http://blog.bugxch.top" target="_blank" rel="noopener"&#10; &gt;巴巴变的博客&lt;/a&gt;，遵循CC BY-NC-SA 4.0协议，复制请保留原文出处。&lt;/p&gt;&#10;</description></item></channel></rss>