<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Version Control on kenji.blog</title><link>http://kenji.blog/zh-cn/tags/version-control/</link><description>Recent content in Version Control on kenji.blog</description><generator>Hugo -- gohugo.io</generator><language>zh-cn</language><copyright>kenjinote</copyright><lastBuildDate>Sun, 13 Sep 2026 09:00:00 +0900</lastBuildDate><atom:link href="http://kenji.blog/zh-cn/tags/version-control/index.xml" rel="self" type="application/rss+xml"/><item><title>【Git命令】rebase与merge的区别，以及在实际工作中的正确使用场景</title><link>http://kenji.blog/zh-cn/p/git-rebase-vs-merge-practical-guide/</link><pubDate>Sun, 13 Sep 2026 09:00:00 +0900</pubDate><guid>http://kenji.blog/zh-cn/p/git-rebase-vs-merge-practical-guide/</guid><description>&lt;img src="http://kenji.blog/p/git-rebase-vs-merge-practical-guide/img/eyecatch.jpg" alt="Featured image of post 【Git命令】rebase与merge的区别，以及在实际工作中的正确使用场景" />&lt;h1 id="1-前言为什么merge还是rebase是一个永恒的难题">1. 前言：为什么“merge还是rebase”是一个永恒的难题
&lt;/h1>&lt;p>Git是现代软件开发中不可或缺的版本控制系统。当多名开发者同时修改代码库时，Git强大的分支模型发挥了巨大的作用。然而，在团队开发中“应该使用 &lt;code>merge&lt;/code> 还是 &lt;code>rebase&lt;/code>”的争论，是无论初学者还是资深开发者都经常面临的话题。&lt;/p>
&lt;p>本文将从Git的内部结构（即DAG，有向无环图）以及提交哈希的数学性质出发，深入剖析 &lt;code>git merge&lt;/code> 和 &lt;code>git rebase&lt;/code> 的机制差异。并且，结合具体的实际工作流，彻底讲解在实际工作中应该如何区分使用它们。不仅仅是介绍命令，通过理解Git在后台进行怎样的计算，你将不再恐惧冲突，并能够构建出清晰且可追踪的历史记录。&lt;/p>
&lt;hr>
&lt;h1 id="2-git的内部结构提交哈希与对象模型">2. Git的内部结构：提交哈希与对象模型
&lt;/h1>&lt;p>为了理解Git是如何整合历史记录的，首先需要知道Git是如何保存数据的。Git并不是仅仅保存文件更改的差异（补丁），而是保存某个时间点整个文件系统的快照。&lt;/p>
&lt;h2 id="21-提交哈希的密码学性质">2.1 提交哈希的密码学性质
&lt;/h2>&lt;p>Git的每个提交（Commit）都通过SHA-1（安全哈希算法1）哈希函数计算出一个40位的十六进制字符串来唯一标识。一个提交对象由以下元素构成：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>指向Tree对象的指针&lt;/strong>: 此时的目录结构和文件（Blob）的快照&lt;/li>
&lt;li>&lt;strong>指向父提交的指针&lt;/strong>: 一个或多个父提交的哈希值（首次提交没有父提交，合并提交有两个或以上的父提交）&lt;/li>
&lt;li>&lt;strong>作者信息（Author）&lt;/strong>: 编写代码的人和时间&lt;/li>
&lt;li>&lt;strong>提交者信息（Committer）&lt;/strong>: 创建并应用该提交的人和时间&lt;/li>
&lt;li>&lt;strong>提交信息&lt;/strong>: 解释更改意图的文本&lt;/li>
&lt;/ol>
&lt;p>用数学语言表达，对于提交对象 $C$，其哈希值 $H(C)$ 定义如下：&lt;/p>
$$
H(C) = \text{SHA-1}( \text{tree} \parallel \text{parent} \parallel \text{author} \parallel \text{committer} \parallel \text{message} )
$$&lt;p>这里，$\parallel$ 表示数据的拼接。根据哈希函数的特性，即使提交信息只改变了一个字符，或者父提交不同，也会生成完全不同的哈希值。也就是说，&lt;strong>提交是不可变（Immutable）的&lt;/strong>。后文提到的 &lt;code>rebase&lt;/code> 被称为“重写历史”，实际上是因为“它创建了内容相似但哈希值不同的全新提交”。&lt;/p>
&lt;p>哈希空间的大小为 $2^{160}$，发生碰撞（不同的提交具有相同的哈希值）的概率 $P$，可以使用生日攻击（Birthday Paradox）的理论进行如下近似（$n$ 为提交数）：&lt;/p>
$$
P(\text{collision}) \approx 1 - \exp\left(-\frac{n^2}{2 \times 2^{160}}\right)
$$&lt;p>这个概率极低，在实际应用中Git的提交哈希几乎不可能发生碰撞。&lt;/p>
&lt;hr>
&lt;h1 id="3-图论与daggit历史的数学模型">3. 图论与DAG：Git历史的数学模型
&lt;/h1>&lt;p>Git的提交历史可以被建模为图论中的“有向无环图（Directed Acyclic Graph, DAG）”。&lt;/p>
&lt;h2 id="31-什么是dag有向无环图">3.1 什么是DAG（有向无环图）
&lt;/h2>&lt;p>在图 $G = (V, E)$ 中，$V$ 是提交的集合（顶点），$E$ 是表示提交之间父子关系的有向边的集合。在Git中，边的方向是“从子提交指向父提交”。这是因为新的提交持有着指向过去提交的指针。&lt;/p>
&lt;pre class="mermaid">
graph BT
A[&amp;#34;提交 A (初始)&amp;#34;]
B[&amp;#34;提交 B&amp;#34;]
C[&amp;#34;提交 C (Main)&amp;#34;]
D[&amp;#34;提交 D (Feature)&amp;#34;]
E[&amp;#34;提交 E (Merge)&amp;#34;]
B --&amp;gt; A
C --&amp;gt; B
D --&amp;gt; B
E --&amp;gt; C
E --&amp;gt; D
&lt;/pre>
&lt;p>DAG的最大特征是“不存在环（循环）”。正因如此，追溯提交历史的算法不会陷入死循环，并且必定能到达终点（初始提交）。&lt;/p>
&lt;h2 id="32-拓扑排序与历史顺序">3.2 拓扑排序与历史顺序
&lt;/h2>&lt;p>当使用 &lt;code>git log&lt;/code> 等命令显示历史记录时，DAG会通过拓扑排序（Topological Sort）算法被排序为一个一维列表。对于DAG中任意有向边 $u \to v$（$u$ 是 $v$ 的子节点），在列表中 $u$ 会被排列在 $v$ 的前面。&lt;/p>
&lt;hr>
&lt;h1 id="4-git-merge-的机制与种类">4. git merge 的机制与种类
&lt;/h1>&lt;p>整合分支更改最基本的命令是 &lt;code>git merge&lt;/code>。但是，根据当前的状态，Git会自动选择不同的合并策略。&lt;/p>
&lt;h2 id="41-fast-forward-合并--ff">4.1 Fast-Forward 合并（&amp;ndash;ff）
&lt;/h2>&lt;p>当目标分支（例：&lt;code>main&lt;/code>）是来源分支（例：&lt;code>feature&lt;/code>）的直接祖先时，Git会执行“Fast-Forward（快进）”合并。这不会创建新的提交，只是将分支的指针向前移动而已。&lt;/p>
&lt;pre class="mermaid">
gitGraph
commit id: &amp;#34;A&amp;#34;
commit id: &amp;#34;B&amp;#34;
branch feature
checkout feature
commit id: &amp;#34;C&amp;#34;
commit id: &amp;#34;D&amp;#34;
checkout main
merge feature
&lt;/pre>
&lt;p>Fast-Forward合并能保持历史记录为一条直线，但缺点是会丢失“哪些提交是作为一个功能开发（feature）组合在一起的”这一上下文信息。&lt;/p>
&lt;h2 id="42-non-fast-forward-合并--no-ff">4.2 Non-Fast-Forward 合并（&amp;ndash;no-ff）
&lt;/h2>&lt;p>如果显式指定 &lt;code>git merge --no-ff&lt;/code>，即使处于可以Fast-Forward的情况下，也必定会创建一个新的“合并提交（Merge Commit）”。合并提交是具有两个父提交的特殊提交。&lt;/p>
&lt;pre class="mermaid">
gitGraph
commit id: &amp;#34;A&amp;#34;
commit id: &amp;#34;B&amp;#34;
branch feature
checkout feature
commit id: &amp;#34;C&amp;#34;
commit id: &amp;#34;D&amp;#34;
checkout main
commit id: &amp;#34;Main Work 1&amp;#34;
merge feature type: NORMAL
&lt;/pre>
&lt;p>这种方法的优点是，功能分支的存在和历史会清晰地保留在DAG上。当发生问题时，可以通过执行 &lt;code>git revert -m 1 &amp;lt;合并提交的哈希值&amp;gt;&lt;/code>，一次性安全地撤销（revert）整个功能。&lt;/p>
&lt;h2 id="43-三方合并3-way-merge算法">4.3 三方合并（3-Way Merge）算法
&lt;/h2>&lt;p>如果目标分支和来源分支都各自拥有独有的提交，Git就会执行三方合并。此时，Git会遍历DAG，找出两个分支的“最近公共祖先（Lowest Common Ancestor, LCA）”。&lt;/p>
&lt;p>寻找LCA的算法的时间复杂度 $T_{\text{LCA}}$ 相对于顶点数 $|V|$ 和边数 $|E|$ 是线性时间：&lt;/p>
$$
T_{\text{LCA}} = \mathcal{O}(|V| + |E|)
$$&lt;p>Git会将“LCA的状态”、“当前分支的状态”和“对方分支的状态”这三者进行比较，如果没有冲突，就会自动生成一个合并提交。&lt;/p>
&lt;hr>
&lt;h1 id="5-git-rebase-的机制与历史的重构">5. git rebase 的机制与历史的重构
&lt;/h1>&lt;p>&lt;code>git merge&lt;/code> 是“整合”历史记录，而 &lt;code>git rebase&lt;/code> 则是“重构（变基）”历史记录。&lt;/p>
&lt;h2 id="51-rebase背后的运行机制">5.1 Rebase背后的运行机制
&lt;/h2>&lt;p>将 &lt;code>feature&lt;/code> 分支变基到 &lt;code>main&lt;/code> 分支（&lt;code>git rebase main&lt;/code>）时的内部动作如下：&lt;/p>
&lt;ol>
&lt;li>找到 &lt;code>feature&lt;/code> 分支和 &lt;code>main&lt;/code> 分支的最近公共祖先（LCA）。&lt;/li>
&lt;li>将从 LCA 到 &lt;code>feature&lt;/code> 分支最新提交之间的差异保存到临时区域。&lt;/li>
&lt;li>将 &lt;code>feature&lt;/code> 分支的指针移动到 &lt;code>main&lt;/code> 分支的最新提交。&lt;/li>
&lt;li>将保存的差异按照顺序，逐个应用（Cherry-Pick）到新的基底（&lt;code>main&lt;/code>的最新提交）上，并生成新的提交。&lt;/li>
&lt;/ol>
&lt;pre class="mermaid">
graph TD
A[&amp;#34;提交 A&amp;#34;] --&amp;gt; B[&amp;#34;提交 B&amp;#34;]
B --&amp;gt; C[&amp;#34;提交 C (Main)&amp;#34;]
B --&amp;gt; D[&amp;#34;提交 D (旧 Feature)&amp;#34;]
D -.-&amp;gt; E[&amp;#34;提交 D&amp;#39; (新 Feature)&amp;#34;]
C --&amp;gt; E
style D stroke-dasharray: 5 5, fill: #f9f9f9, color: #999
&lt;/pre>
&lt;p>这里的关键是，通过变基生成的新提交 $D'$ 与原提交 $D$ &lt;strong>因为父提交不同，所以拥有完全不同的哈希值&lt;/strong>（参考前文的哈希函数定义 $H(C)$）。&lt;/p>
&lt;h2 id="52-交互式变基interactive-rebase">5.2 交互式变基（Interactive Rebase）
&lt;/h2>&lt;p>使用 &lt;code>git rebase -i&lt;/code>（或 &lt;code>--interactive&lt;/code>），可以随心所欲地操作提交历史。这是用于整理本地历史的最强工具。&lt;/p>
&lt;ul>
&lt;li>&lt;code>pick&lt;/code> : 原样保留该提交&lt;/li>
&lt;li>&lt;code>reword&lt;/code> : 仅修改提交信息&lt;/li>
&lt;li>&lt;code>edit&lt;/code> : 暂停以便修改提交内容&lt;/li>
&lt;li>&lt;code>squash&lt;/code> : 将该提交融合到前一个提交中，并合并提交信息&lt;/li>
&lt;li>&lt;code>fixup&lt;/code> : 与 &lt;code>squash&lt;/code> 相同，但会丢弃该提交的提交信息&lt;/li>
&lt;li>&lt;code>drop&lt;/code> : 完全删除该提交&lt;/li>
&lt;/ul>
&lt;p>从数学角度来看，如果某个分支有 $N$ 个提交，通过重新排列变基顺序可以生成的线性历史的变化（排列组合） $P$ 如下：&lt;/p>
$$
P = N!
$$&lt;p>Git赋予了开发者 $N!$ 种自由，使其能够将历史记录保持在充满逻辑感和美感的状态。&lt;/p>
&lt;hr>
&lt;h1 id="6-变基的黄金法则the-golden-rule-of-rebase">6. 变基的黄金法则（The Golden Rule of Rebase）
&lt;/h1>&lt;p>&lt;code>rebase&lt;/code> 虽然非常强大，但有一个绝对的规则。&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>“绝对不要对已公开的公共历史进行变基”&lt;/strong>
&lt;em>(Never rebase public history)&lt;/em>&lt;/p>
&lt;/blockquote>
&lt;h2 id="61-为什么不能对公共历史进行变基">6.1 为什么不能对公共历史进行变基？
&lt;/h2>&lt;p>Git是分布式的。你推送到 &lt;code>origin/main&lt;/code> 的提交，已经被克隆（复制）到了其他开发者的本地仓库中。如果你对已经推送的提交进行变基并重写了历史，然后使用 &lt;code>git push --force&lt;/code> 强制覆盖，会发生什么呢？&lt;/p>
&lt;p>其他开发者本地的DAG和远程的DAG会在根本上产生分歧。当其他开发者执行 &lt;code>git pull&lt;/code> 时，Git会强行尝试合并拥有不同历史的提交群，从而产生大量的冲突和重复提交（内容相同但哈希值不同的提交），导致仓库陷入混乱。&lt;/p>
&lt;p>变基的铁则是：&lt;strong>只能对“尚未与任何人共享的、本地的分支”进行操作&lt;/strong>。&lt;/p>
&lt;hr>
&lt;h1 id="7-解决冲突与-git-rebase---continue">7. 解决冲突与 git rebase &amp;ndash;continue
&lt;/h1>&lt;p>当多人修改了同一个文件的同一个地方时，就会产生冲突。&lt;code>merge&lt;/code> 和 &lt;code>rebase&lt;/code> 解决冲突的过程是不同的。&lt;/p>
&lt;h2 id="71-merge中的冲突解决">7.1 Merge中的冲突解决
&lt;/h2>&lt;p>在使用 &lt;code>git merge&lt;/code> 时，冲突的解决&lt;strong>只会发生一次&lt;/strong>。在创建最终的合并提交之前，一次性修复所有的冲突处。&lt;/p>
&lt;h2 id="72-rebase中的冲突解决">7.2 Rebase中的冲突解决
&lt;/h2>&lt;p>在使用 &lt;code>git rebase&lt;/code> 时，由于其逐个重新应用提交的性质，&lt;strong>在每个提交处都有可能发生冲突&lt;/strong>。&lt;/p>
&lt;p>在变基过程中如果发生冲突，Git会暂停处理。解决的流程如下：&lt;/p>
&lt;ol>
&lt;li>打开编辑器或IDE（如VS Code），手动修复冲突标记（&lt;code>&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&lt;/code>、&lt;code>======&lt;/code> 和 &lt;code>&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&lt;/code>）。&lt;/li>
&lt;li>将修复后的文件添加到暂存区：
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git add &amp;lt;修改后的文件&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;li>不要创建提交，直接继续变基流程：
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git rebase --continue
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;/ol>
&lt;p>如果想中止变基并恢复到原来的状态，可以执行以下命令：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git rebase --abort
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>（※如果不需要解决冲突，想要直接跳过那个提交，可以使用 &lt;code>git rebase --skip&lt;/code>）&lt;/p>
&lt;hr>
&lt;h1 id="8-实际工作中的正确使用场景工作流实践">8. 实际工作中的正确使用场景（工作流实践）
&lt;/h1>&lt;p>那么，在实际的开发场景中，我们应该如何区分使用 &lt;code>merge&lt;/code> 和 &lt;code>rebase&lt;/code> 呢？这里介绍最标准且最安全的方法。&lt;/p>
&lt;h2 id="81-场景1整理本地开发历史使用-rebase">8.1 【场景1】整理本地开发历史（使用 Rebase）
&lt;/h2>&lt;p>假设在功能分支上开发时，积累了许多细碎的提交（比如“修复拼写错误”、“临时保存”等）。在发起拉取请求（PR）之前，为了将这些提交整理成有意义的单元，可以使用交互式变基。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 在 feature 分支下执行&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git rebase -i HEAD~5
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># （编辑器将打开，使用 squash 或 fixup 让历史变得整洁）&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>通过这种方式，可以制作出意图清晰、美观的提交历史，便于审查者理解。&lt;/p>
&lt;h2 id="82-场景2同步最新的-main-分支使用-rebase">8.2 【场景2】同步最新的 main 分支（使用 Rebase）
&lt;/h2>&lt;p>如果开发周期较长，并且不断有其他人的修改被合并到 &lt;code>main&lt;/code> 分支中，你自己的 &lt;code>feature&lt;/code> 分支就会变旧。在这种情况下，需要将 &lt;code>feature&lt;/code> 分支变基到最新的 &lt;code>main&lt;/code> 分支上以保持同步。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 获取 main 的最新信息&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git fetch origin
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 将 feature 分支重新基底到最新的 main 上&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git rebase origin/main
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>这样可以使历史记录保持为一条直线，防止后续合并时发生冲突。同时，也能避免生成多余的合并提交（例如 &amp;ldquo;Merge branch &amp;lsquo;main&amp;rsquo; into feature&amp;rdquo;）。&lt;/p>
&lt;h2 id="83-场景3整合已完成的功能使用-merge">8.3 【场景3】整合已完成的功能（使用 Merge）
&lt;/h2>&lt;p>当 &lt;code>feature&lt;/code> 分支的开发完成，终于进入要整合到 &lt;code>main&lt;/code> 分支的阶段。在这里我们使用 &lt;strong>&lt;code>git merge --no-ff&lt;/code>&lt;/strong>（这与在 GitHub 等的 Pull Request 中选择“Create a merge commit”是一样的）。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git checkout main
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git merge --no-ff feature -m &lt;span class="s2">&amp;#34;Merge feature: 实现用户登录功能&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git push origin main
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>这样一来，在 &lt;code>main&lt;/code> 分支的DAG上就会留下一个“这里合并了一个功能”的历史节点（合并提交）。日后查看历史记录时，也能更容易地以功能为单位追踪代码。&lt;/p>
&lt;hr>
&lt;h1 id="9-结论总结">9. 结论（总结）
&lt;/h1>&lt;p>在Git操作中，“一切都用Merge解决”或“一切都用Rebase保持直线”这样极端的做法，都有各自的优缺点。&lt;/p>
&lt;p>实际工作中的最佳实践是：**“本地的私有历史使用 rebase 进行整理，使其美观；而公共的整合历史使用 merge &amp;ndash;no-ff 来保留上下文”**的混合模式。&lt;/p>
&lt;ul>
&lt;li>&lt;strong>本地（个人工作区）&lt;/strong>: 使用 &lt;code>rebase&lt;/code> 排除无用的提交，紧跟最新的主线，保持直线的历史记录。&lt;/li>
&lt;li>&lt;strong>全局（共享工作区）&lt;/strong>: 使用 &lt;code>merge --no-ff&lt;/code>，将功能分支的存在作为合并提交记录在DAG中，便于后续的追踪和撤销（revert）。&lt;/li>
&lt;/ul>
&lt;p>通过理解DAG结构和哈希函数机制等数学及架构层面的背景，Git命令就不再是死记硬背，而是升华为“有目的的历史记录设计”。在遵守变基黄金法则的前提下，根据具体情况选择最合适的命令，为整个团队构建一个易于阅读和维护的整洁的提交历史吧。&lt;/p></description></item><item><title>Git初学者容易陷入的误区与解决命令集（冲突解决等）</title><link>http://kenji.blog/zh-cn/p/git-beginners-mistakes-and-solutions/</link><pubDate>Sat, 12 Sep 2026 17:00:00 +0900</pubDate><guid>http://kenji.blog/zh-cn/p/git-beginners-mistakes-and-solutions/</guid><description>&lt;img src="http://kenji.blog/p/git-beginners-mistakes-and-solutions/img/eyecatch.jpg" alt="Featured image of post Git初学者容易陷入的误区与解决命令集（冲突解决等）" />&lt;h1 id="git初学者容易陷入的误区与解决命令集冲突解决等">Git初学者容易陷入的误区与解决命令集（冲突解决等）
&lt;/h1>&lt;h2 id="1-引言为什么会在git上犯错">1. 引言：为什么会在Git上犯错？
&lt;/h2>&lt;p>在软件开发中，Git已经成为了像空气和水一样不可或缺的存在。然而，对于许多初学者（甚至有时是老手）来说，Git可能会让人觉得像是一个“可怕的魔法黑盒”。提交记录消失了、在意外的分支中塞入了大量更改、或者屏幕上满是见都没见过的冲突错误信息……。一旦陷入这种“Git的陷阱”，工作进度就会完全停滞，最糟糕的时候甚至会被“是不是把源代码给搞坏了”的恐惧所支配。&lt;/p>
&lt;p>为什么Git会如此困难，并且容易诱发错误呢？最大的原因是：“没有理解Git内部到底发生了什么，仅仅是死记硬背表面上的命令来使用”。Git虽然是基于分布式版本控制系统（DVCS）的健壮设计理念构建的，但其接口（CLI）并不总是直观的。&lt;/p>
&lt;p>本文将针对Git初学者在实际工作中经常遇到的“失误（常见错误）”进行多种情况的分类，并提供各自具体的解决命令。然而，这不会仅仅是命令的罗列（速查表）。“为什么会发生这个错误”、“执行该命令时Git内部的数据是如何变动的”，我们将结合 &lt;code>.git&lt;/code> 目录的结构、背后运行的Diff算法的数学背景，以及Mermaid图解，通过超过10,000字的篇幅来进行彻底深挖。&lt;/p>
&lt;p>当读完这篇文章时，你将从“Git很可怕”的恐惧中解放出来，反而会确信“没有比Git更值得信赖的伙伴了”。那么，让我们一起潜入Git的深渊世界吧。&lt;/p>
&lt;hr>
&lt;h2 id="2-git的深渊理解-git-目录的内部结构">2. Git的深渊：理解 &lt;code>.git&lt;/code> 目录的内部结构
&lt;/h2>&lt;p>要让许多故障排查变得简单，第一步就是了解Git是如何保存数据的。存在于你项目根目录中的隐藏文件夹 &lt;code>.git&lt;/code>，正是Git的心脏。Git并不是顺次记录简单文件差异（补丁）的系统，而是将数据作为**快照流（Stream of snapshots）**来进行管理。&lt;/p>
&lt;h3 id="21-对象模型blob-tree-commit">2.1 对象模型：Blob, Tree, Commit
&lt;/h3>&lt;p>Git主要使用3种对象来表示仓库的状态。这些对象保存在 &lt;code>.git/objects&lt;/code> 中。&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Blob (Binary Large Object)&lt;/strong>
保存文件内容本身的对象。文件名和权限信息不包含在此处。纯字节流会经过zlib压缩，并通过SHA-1散列值（40个字符的十六进制数）来识别。&lt;/li>
&lt;li>&lt;strong>Tree&lt;/strong>
表示目录结构的对象。Tree对象包含指向其他Tree对象（子目录）或Blob对象（文件）的指针（SHA-1散列值），以及它们的文件名和访问权限。它扮演着类似UNIX目录的角色。&lt;/li>
&lt;li>&lt;strong>Commit&lt;/strong>
保存指向某一时点整个仓库顶层Tree对象的指针，以及元数据（作者、提交日期、提交信息），还有指向前一次提交（父提交）的指针。&lt;/li>
&lt;/ol>
&lt;pre class="mermaid">
graph TD
Commit1[&amp;#34;提交 (Hash: 9f8a)&amp;#34;] --&amp;gt; Tree1[&amp;#34;树 (Hash: 4b82)&amp;#34;]
Tree1 --&amp;gt; Blob1[&amp;#34;数据块 (Hash: 8d7e) : index.js&amp;#34;]
Tree1 --&amp;gt; Tree2[&amp;#34;树 (Hash: 3a2c) : src/&amp;#34;]
Tree2 --&amp;gt; Blob2[&amp;#34;数据块 (Hash: 5f1b) : app.js&amp;#34;]
&lt;/pre>
&lt;h3 id="22-head与引用refs的真面目">2.2 HEAD与引用（Refs）的真面目
&lt;/h3>&lt;p>在Git中工作时，我们会频繁地看到 &lt;code>HEAD&lt;/code> 这个词。这是一个&lt;strong>符号引用（Symbolic Reference）&lt;/strong>，指向当前检出的分支（或提交）。
用文本编辑器打开 &lt;code>.git/HEAD&lt;/code> 文件，会看到类似下面的字符串：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">ref: refs/heads/main
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>这意味着“当前状态位于 &lt;code>main&lt;/code> 分支的顶端”。接着，打开 &lt;code>.git/refs/heads/main&lt;/code>，里面写着一个40字符的SHA-1散列值，这就是指向最新Commit对象的指针。
Git的分支，仅仅是一个指向特定提交的轻量级指针（文件）。只需了解这个事实，就能消除“如果删除分支，文件是不是就全没了？”的恐惧。&lt;/p>
&lt;hr>
&lt;h2 id="3-从数学角度解读gitdiff算法与散列函数">3. 从数学角度解读Git：Diff算法与散列函数
&lt;/h2>&lt;p>当Git检测冲突或显示文件差异时，内部运行着高级的算法。&lt;/p>
&lt;h3 id="31-myers的diff算法">3.1 Myers的Diff算法
&lt;/h3>&lt;p>Git默认的差异检测算法是由Eugene W. Myers提出的算法。当有2个文本文件 $A$ 和 $B$ 时，寻找将 $A$ 转换为 $B$ 的“最小编辑步骤（插入和删除）”的问题，可以在图论中建模为最短路径问题。&lt;/p>
&lt;p>设字符串的长度分别为 $N, M$，总和为 $V = N + M$。在Myers算法中，我们要寻找编辑距离（Edit Distance） $D$。该算法的时间复杂度由以下公式表示：&lt;/p>
$$ \mathcal{O}(V \cdot D) $$&lt;p>这里，当文件间的差异较小（即 $D$ 较小）时，算法会以非常快的 $\mathcal{O}(V)$ 速度运行。但是，如果文件完全不同，则 $D \approx V$，最坏情况的时间复杂度变为 $\mathcal{O}(V^2)$。&lt;/p>
&lt;h3 id="32-patience-diff-与-histogram-diff">3.2 Patience Diff 与 Histogram Diff
&lt;/h3>&lt;p>Myers算法非常优秀，但在大幅度改变函数或类的顺序时，有时会生成对人类来说不直观（缺乏意义）的差异。为了解决这个问题，Git实现了 &lt;code>Patience Diff&lt;/code> 和 &lt;code>Histogram Diff&lt;/code>。&lt;/p>
&lt;p>Patience Diff 着眼于“在两个文件中只出现一次的唯一行”，并寻找它们的最长公共子序列（Longest Common Subsequence: LCS）。假设唯一元素的数量为 $U$，则LCS的计算可以用以下时间复杂度解决：&lt;/p>
$$ \mathcal{O}(U \log U) $$&lt;p>当你觉得冲突很难解决时，使用 &lt;code>git diff --histogram&lt;/code>，或者在合并策略中指定该算法（&lt;code>git merge -s recursive -X histogram&lt;/code>）是一个好办法。&lt;/p>
&lt;h3 id="33-sha-1与碰撞概率">3.3 SHA-1与碰撞概率
&lt;/h3>&lt;p>Git通过SHA-1散列值来管理所有对象。散列空间的大小为 $2^{160}$。关于散列碰撞（不同的内容具有相同的散列值）的概率，使用生日悖论（Birthday Paradox）进行近似，碰撞概率 $p$ 达到50%所需的对象数量 $k$ 如下：&lt;/p>
$$ k \approx \sqrt{2 \ln(2)} \cdot 2^{80} \approx 1.2 \times 2^{80} $$&lt;p>这是一个天文数字，在通常的软件开发中，意外发生碰撞的概率实际上为零。因此，Git可以信赖地将散列值作为“绝对唯一的ID”来运行。&lt;/p>
&lt;hr>
&lt;h2 id="4-案例研究1提交到了错误的分支上">4. 案例研究1：提交到了错误的分支上！
&lt;/h2>&lt;p>&lt;strong>【场景】&lt;/strong>
没有意识到自己正在 &lt;code>main&lt;/code> 分支上工作，大刀阔斧地写了新功能的代码，甚至还顺手执行了 &lt;code>git commit&lt;/code>。本来是应该创建一个 &lt;code>feature/login&lt;/code> 分支并在那里工作的！&lt;/p>
&lt;h3 id="解决方法使用-git-reset-和创建分支">解决方法：使用 &lt;code>git reset&lt;/code> 和创建分支
&lt;/h3>&lt;p>在Git中，提交是独立的对象，分支只是指针。因此，只需通过“先创建一个新分支，然后将当前分支的指针回退”的操作，就能瞬间解决。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;span class="lnt">8
&lt;/span>&lt;span class="lnt">9
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 1. 创建一个新分支，指向当前的提交（错误创建的提交）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ git branch feature/login
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 2. 将main分支的指针回退到上一个提交（HEAD~1）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 使用 --keep 可以在保留工作目录未提交的更改的同时安全地重置。&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ git reset --keep HEAD~1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 3. 切换到正确的分支&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ git checkout feature/login
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h3 id="图解内部发生了什么">图解：内部发生了什么？
&lt;/h3>&lt;p>让我们使用Mermaid的 &lt;code>gitGraph&lt;/code>，将此时的分支指针移动可视化。&lt;/p>
&lt;pre class="mermaid">
gitGraph
commit id: &amp;#34;初始提交&amp;#34;
commit id: &amp;#34;修复Bug&amp;#34;
commit id: &amp;#34;错误的提交&amp;#34; type: HIGHLIGHT
branch feature/login
checkout feature/login
checkout main
&lt;/pre>
&lt;p>最初 &lt;code>main&lt;/code> 和 &lt;code>HEAD&lt;/code> 都指向 &amp;ldquo;错误的提交&amp;rdquo;，但是通过 &lt;code>git branch feature/login&lt;/code>，在那个位置创建了一个新指针。之后通过 &lt;code>git reset&lt;/code>，只有 &lt;code>main&lt;/code> 的指针回到了 &amp;ldquo;修复Bug&amp;rdquo; 的位置。对象本身并没有任何内容被删除。&lt;/p>
&lt;hr>
&lt;h2 id="5-案例研究2想要撤销已经推送的提交">5. 案例研究2：想要撤销已经推送的提交！
&lt;/h2>&lt;p>&lt;strong>【场景】&lt;/strong>
深夜精神亢奋时写下并提交了一堆充满Bug的代码，而且还通过 &lt;code>git push origin main&lt;/code> 将其推送到远程仓库公开了。发现严重Bug后面如死灰。&lt;/p>
&lt;h3 id="解决方案1抵消历史的-git-revert-推荐安全">解决方案1：抵消历史的 &lt;code>git revert&lt;/code> （推荐·安全）
&lt;/h3>&lt;p>在团队开发中，绝对禁止使用 &lt;code>git reset&lt;/code> 等命令篡改已经推送到远程的提交历史。这会导致与其他开发者的本地仓库失去同步。正确的做法是：&lt;strong>“创建一个新的相反提交，来完全抵消那个错误提交的更改”&lt;/strong>。这就是 &lt;code>git revert&lt;/code>。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 创建一个抵消最新提交的新提交&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ git revert HEAD
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">[&lt;/span>main 7f3a8b2&lt;span class="o">]&lt;/span> Revert &lt;span class="s2">&amp;#34;错误的提交信息&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="m">1&lt;/span> file changed, &lt;span class="m">1&lt;/span> insertion&lt;span class="o">(&lt;/span>+&lt;span class="o">)&lt;/span>, &lt;span class="m">10&lt;/span> deletions&lt;span class="o">(&lt;/span>-&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 推送到远程&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ git push origin main
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;pre class="mermaid">
gitGraph
commit id: &amp;#34;提交 A&amp;#34;
commit id: &amp;#34;提交 B (错误)&amp;#34;
commit id: &amp;#34;撤销提交 B&amp;#34; type: REVERSE
&lt;/pre>
&lt;p>历史继续推进，只是代码的状态恢复到了原来的样子。&lt;/p>
&lt;h3 id="解决方案2篡改历史的-git-push---force-with-lease">解决方案2：篡改历史的 &lt;code>git push --force-with-lease&lt;/code>
&lt;/h3>&lt;p>如果你是在一个只有你一个人使用的分支上刚刚进行了推送，那么重写历史也是可以接受的。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 在本地重置提交并进行修复&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ git reset --hard HEAD~1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ git add .
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ git commit -m &lt;span class="s2">&amp;#34;正确的实现&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 强制覆盖远程的历史&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ git push origin feature/login --force-with-lease
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;code>--force-with-lease&lt;/code> 是一种安全的强制推送，可以防止意外覆盖他人的工作。&lt;/p>
&lt;hr>
&lt;h2 id="6-案例研究3工作中途想要切换到其他分支stash的魔法">6. 案例研究3：工作中途想要切换到其他分支（Stash的魔法）
&lt;/h2>&lt;p>&lt;strong>【场景】&lt;/strong>
在 &lt;code>feature/A&lt;/code> 分支上开发新功能，源代码处于连编译都无法通过的半成品状态。突然，上司发来指示：“&lt;code>main&lt;/code> 分支的生产环境有一个紧急Bug，马上修复一下！”&lt;/p>
&lt;h3 id="解决方法通过-git-stash-进行暂存">解决方法：通过 &lt;code>git stash&lt;/code> 进行暂存
&lt;/h3>&lt;p>&lt;code>git stash&lt;/code> 是一个将未提交的更改避难到临时区域的命令。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 1. 暂存工作中的更改&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ git stash push -m &lt;span class="s2">&amp;#34;WIP: feature A 部分实现&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 2. 现在可以切换到main分支了&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ git checkout main
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># ...（执行紧急Bug修复工作，提交并推送）...&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 3. 工作结束后返回原分支&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ git checkout feature/A
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 4. 恢复之前暂存的更改&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ git stash pop
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>执行 &lt;code>git stash&lt;/code> 时，Git内部会生成2个特殊的提交对象，并保存在 &lt;code>refs/stash&lt;/code> 这个引用中。也就是说，Stash归根结底也是一种“没有名字的临时提交”。&lt;/p>
&lt;hr>
&lt;h2 id="7-案例研究4令人恐惧的-detached-head-状态">7. 案例研究4：令人恐惧的 &amp;ldquo;Detached HEAD&amp;rdquo; 状态
&lt;/h2>&lt;p>&lt;strong>【场景】&lt;/strong>
为了确认过去某个特定时间点的代码，执行了 &lt;code>git checkout 9f8a7b6&lt;/code>。结果提示 &lt;code>You are in 'detached HEAD' state.&lt;/code>。我就这么直接提交了，但是一切换分支，刚才的提交就不见了！&lt;/p>
&lt;h3 id="detached-head的机制">Detached HEAD的机制
&lt;/h3>&lt;p>通常 &lt;code>HEAD&lt;/code> 指向 &lt;code>refs/heads/main&lt;/code> 等分支。但是，如果你直接检出一个特定的提交，&lt;code>HEAD&lt;/code> 就会直接指向这个提交对象。这被称为 &lt;strong>Detached HEAD（游离的HEAD / 分离的HEAD）&lt;/strong>。&lt;/p>
&lt;pre class="mermaid">
graph TD
A[&amp;#34;提交 A&amp;#34;] --&amp;gt; B[&amp;#34;提交 B&amp;#34;]
B --&amp;gt; C[&amp;#34;提交 C&amp;#34;]
C --&amp;gt; D[&amp;#34;提交 D&amp;#34;]
BranchMain[&amp;#34;分支: main&amp;#34;] --&amp;gt; D
HEAD[&amp;#34;HEAD&amp;#34;] --&amp;gt; B
style HEAD fill:#f9f,stroke:#333,stroke-width:4px
&lt;/pre>
&lt;p>在这种状态下即使叠加了新的提交，任何分支都不会跟踪那个新提交。当切换到其他分支的瞬间，新的提交就成了迷路的孩子。&lt;/p>
&lt;h3 id="解决方案将其保存为新分支">解决方案：将其保存为新分支
&lt;/h3>&lt;p>只要在当前所处的位置创建一个新分支即可解决。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 在当前HEAD的位置创建一个新分支，并切换过去&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ git checkout -b feature/recovered-work
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;hr>
&lt;h2 id="8-案例研究5合并与变基时的冲突解决">8. 案例研究5：合并与变基时的冲突解决
&lt;/h2>&lt;p>&lt;strong>【场景】&lt;/strong>
执行了 &lt;code>git merge&lt;/code> 或 &lt;code>git rebase&lt;/code> 后，提示 &lt;code>CONFLICT (content)&lt;/code>，并且流程被中断了。&lt;/p>
&lt;h3 id="合并merge与变基rebase的区别">合并（Merge）与变基（Rebase）的区别
&lt;/h3>&lt;ol>
&lt;li>&lt;strong>Merge（合并）&lt;/strong>
使用两个分支的最新提交和它们共同的祖先进行三方合并（3-way merge），并创建一个合并提交（Merge commit）。&lt;/li>
&lt;li>&lt;strong>Rebase（变基）&lt;/strong>
将当前分支的提交临时保存，并重新应用到目标分支的顶端。这会使历史保持一条直线。&lt;/li>
&lt;/ol>
&lt;pre class="mermaid">
gitGraph
commit id: &amp;#34;M1&amp;#34;
commit id: &amp;#34;M2&amp;#34;
branch feature
checkout feature
commit id: &amp;#34;F1&amp;#34;
commit id: &amp;#34;F2&amp;#34;
checkout main
commit id: &amp;#34;M3&amp;#34;
merge feature
&lt;/pre>
&lt;h3 id="解决冲突的方法">解决冲突的方法
&lt;/h3>&lt;p>发生冲突的文件中，被插入了如下的标记：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-javascript" data-lang="javascript">&lt;span class="line">&lt;span class="cl">&lt;span class="o">&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&lt;/span> &lt;span class="nx">HEAD&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kr">const&lt;/span> &lt;span class="nx">apiUrl&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;https://api.production.example.com&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">=======&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kr">const&lt;/span> &lt;span class="nx">apiUrl&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;https://api.staging.example.com&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span> &lt;span class="nx">feature&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="k">new&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="nx">api&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>解决步骤极其简单。&lt;/p>
&lt;ol>
&lt;li>&lt;strong>删除标记，修改为正确的代码。&lt;/strong>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-javascript" data-lang="javascript">&lt;span class="line">&lt;span class="cl">&lt;span class="kr">const&lt;/span> &lt;span class="nx">apiUrl&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">process&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">env&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">NODE_ENV&lt;/span> &lt;span class="o">===&lt;/span> &lt;span class="s1">&amp;#39;production&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">?&lt;/span> &lt;span class="s2">&amp;#34;https://api.production.example.com&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;https://api.staging.example.com&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;li>&lt;strong>将解决后的文件添加到暂存区。&lt;/strong>
&lt;code>git add&lt;/code> 具有“告知Git冲突已解决”的作用。
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">$ git add index.js
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;li>&lt;strong>完成流程。&lt;/strong>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 如果是合并（Merge）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ git commit -m &lt;span class="s2">&amp;#34;Resolve merge conflict in index.js&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 如果是变基（Rebase）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ git rebase --continue
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;/ol>
&lt;p>如果感到慌乱，随时可以通过 &lt;code>$ git merge --abort&lt;/code> 或 &lt;code>$ git rebase --abort&lt;/code> 来中止操作。&lt;/p>
&lt;hr>
&lt;h2 id="9-案例研究6提交历史乱七八糟git-rebase--i">9. 案例研究6：提交历史乱七八糟！&lt;code>git rebase -i&lt;/code>
&lt;/h2>&lt;p>&lt;strong>【场景】&lt;/strong>
“修正错别字”、“再次修正”、“添加测试”等细碎的提交产生了非常多。如果就这样合并到 &lt;code>main&lt;/code> 中，历史记录会变得很脏。&lt;/p>
&lt;h3 id="解决方案交互式变基">解决方案：交互式变基
&lt;/h3>&lt;p>使用 &lt;code>git rebase -i&lt;/code>（interactive），可以重新排列过去提交的顺序，将多个提交合并为一个（squash），或者修改提交信息。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 整理最近的3个提交&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ git rebase -i HEAD~3
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>此时编辑器会打开，显示如下内容：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">pick 1a2b3c4 修正错别字
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">pick 2b3c4d5 再次修正
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">pick 3c4d5e6 添加测试
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>将其修改为以下内容：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">pick 1a2b3c4 功能X的实现
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">squash 2b3c4d5 再次修正
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">squash 3c4d5e6 添加测试
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>保存并关闭后，这3个提交就会被完美地合并为1个。&lt;/p>
&lt;hr>
&lt;h2 id="10-案例研究7不知道bug是什么时候混入的git-bisect">10. 案例研究7：不知道Bug是什么时候混入的！&lt;code>git bisect&lt;/code>
&lt;/h2>&lt;p>&lt;strong>【场景】&lt;/strong>
当前的 &lt;code>main&lt;/code> 分支有Bug，但在1个月前发布时是正常的。想要定位出Bug是在哪个提交中混入的，但是提交数量超过了100个，手动查根本不可能！&lt;/p>
&lt;h3 id="解决方案通过二分查找定位bug">解决方案：通过二分查找定位Bug
&lt;/h3>&lt;p>Git内置了一个工具，可以通过数学上的二分查找（Binary Search）来找出混入Bug的提交。由于时间复杂度为 $\mathcal{O}(\log N)$，即使有1000个提交，只需大约10次测试就能定位出来。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 开始查找&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ git bisect start
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 当前提交存在Bug（bad）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ git bisect bad
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 1个月前（例如Hash为 a1b2c3d）是正常的（good）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ git bisect good a1b2c3d
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Git会自动检出中间的提交，请执行测试&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 如果测试成功：&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ git bisect good
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 如果测试失败：&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ git bisect bad
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>只需重复上述步骤，Git就会准确地告诉你：“这个提交就是第一个Bad提交”。结束后，通过 &lt;code>$ git bisect reset&lt;/code> 即可恢复到原来的状态。&lt;/p>
&lt;hr>
&lt;h2 id="11-终极安全网git-reflog">11. 终极安全网：&lt;code>git reflog&lt;/code>
&lt;/h2>&lt;p>应对Git中任何“失误”的终极奥义就是 &lt;code>git reflog&lt;/code>。Git会将本地所有的操作历史（HEAD的移动历史）完整记录一段时间。即使你删除了分支，或者执行了错误的重置，只需通过 &lt;code>git reflog&lt;/code> 找出过去的Hash值，并在那里执行 &lt;code>git reset --hard&lt;/code>，就能恢复如初。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">$ git reflog
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">9f8a7b6 &lt;span class="o">(&lt;/span>HEAD -&amp;gt; main&lt;span class="o">)&lt;/span> HEAD@&lt;span class="o">{&lt;/span>0&lt;span class="o">}&lt;/span>: commit: Add new feature
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">1a2b3c4 HEAD@&lt;span class="o">{&lt;/span>1&lt;span class="o">}&lt;/span>: reset: moving to HEAD~1
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h2 id="12-结语">12. 结语
&lt;/h2>&lt;p>我们非常详细地讲解了Git初学者容易陷入的误区、背后的Git机制，以及对应的解决方法。提交到错误分支、撤销已推送的提交、活用Stash、从Detached HEAD状态中生还，以及解决冲突。在所有这些操作中，最重要的是要在脑海中想象“Git在背后是如何操作对象和指针的”。&lt;/p>
&lt;p>文件差异是由能够用数学公式表达的严谨Diff算法计算得出的，历史的一致性则是由密码学哈希函数来提供保障的。只要理解了这种优美的设计理念，你就应该能明白：Git绝不是一个“莫名其妙的黑盒”，而是能够坚固守护你源代码的最强护盾。&lt;/p>
&lt;p>下次当你觉得“哎呀搞砸了！”的时候，请不要慌张地关掉终端，深吸一口气，敲入 &lt;code>git status&lt;/code> 吧。Git一定能为你提供恢复的线索。&lt;/p></description></item></channel></rss>