<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Git on Nekonull's Garden</title><link>https://nekonull.me/tags/git/</link><description>Recent content in Git on Nekonull's Garden</description><generator>Hugo</generator><language>zh-CN</language><copyright>CC-BY-SA-4.0</copyright><lastBuildDate>Mon, 07 Oct 2024 13:56:00 +0800</lastBuildDate><atom:link href="https://nekonull.me/tags/git/index.xml" rel="self" type="application/rss+xml"/><item><title>如何手动 squash</title><link>https://nekonull.me/share/how-to-manual-squash/</link><pubDate>Mon, 07 Oct 2024 13:56:00 +0800</pubDate><guid>https://nekonull.me/share/how-to-manual-squash/</guid><description>&lt;p&gt;最近帮解决了一个因为提交流程不规范导致的诡异 Git 分支问题，特此记录下，以备后用。&lt;/p&gt;
&lt;p&gt;背景：主干分支 main，特性分支 feat，在特性分支上开发特性的时候，多次合入了主干分支（仅快进，没有合并冲突）；模拟的 Git 历史如下图所示，其中 &lt;code&gt;m*&lt;/code&gt; 是主干提交，&lt;code&gt;f*&lt;/code&gt; 是特性分支提交&lt;/p&gt;
&lt;div class="td-code td-code--untitled" id="td-code-603fedb9-fence-0" data-td-code data-td-code-auto-id
data-td-language="" data-td-line-count="12"&gt;
&lt;div class="td-code__viewport" id="td-code-603fedb9-fence-0-viewport" data-td-code-viewport&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;* 5fb94b2 (HEAD -&amp;gt; main) m4
| * 83c6299 (feat) f3
| * ff5c5af Merge branch &amp;#39;main&amp;#39; into feat
| |\
| |/
|/|
* | 1698c3c m3
* | 05199a8 m2
| * 68a7e1d f2
| * eb5b169 f1
|/
* ae79b7c m1&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;问题：如何把特性分支上的所有提交合并为一个提交？（类似于 squash 的 Merge 策略，但是手动做到这一点）&lt;/p&gt;
&lt;p&gt;思路：找到主干和特性分支的第一个分叉点，以此为基准生成 patch，然后在一个新分支上 apply patch 得到一个纯净的 commit。&lt;/p&gt;
&lt;p&gt;（非人工写作提示：以下是 LLM 根据我自己的笔记生成的内容。欢迎将反馈贴在评论区，这将决定我以后是否会更积极地使用 LLM 进行创作。）&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;在日常开发中，我们经常会遇到这样的问题：由于开发流程、代码审查或其他原因，特性分支（Feature Branch）和主干（Main Branch）上的提交记录混杂在一起，难以管理。为了让代码历史更为清晰、整洁，我们通常需要手动进行 &lt;code&gt;squash&lt;/code&gt; 操作，将特性分支上的多次提交合并成一个提交点。&lt;/p&gt;
&lt;p&gt;这篇文章将带你一步步了解如何手动进行 &lt;code&gt;squash&lt;/code&gt; 操作，并确保不丢失任何数据。这种方法适用于已经有部分提交合并进主干，并且历史记录较为复杂的场景。&lt;/p&gt;
&lt;h2 id="场景问题"&gt;场景问题&lt;a class="td-heading-self-link" href="#%e5%9c%ba%e6%99%af%e9%97%ae%e9%a2%98" aria-label="标题锚点"&gt;&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;假设你正在开发一个新功能，但在开发过程中，特性分支 &lt;code&gt;feat&lt;/code&gt; 和主干分支 &lt;code&gt;main&lt;/code&gt; 上的提交混杂在一起。这样一来，不仅使代码历史难以追溯，还会影响代码审查和后续维护。因此，我们希望将 &lt;code&gt;feat&lt;/code&gt; 分支上的所有提交整合成一个提交点，并保持代码历史的清晰度。&lt;/p&gt;
&lt;h2 id="前置准备工作"&gt;前置准备工作&lt;a class="td-heading-self-link" href="#%e5%89%8d%e7%bd%ae%e5%87%86%e5%a4%87%e5%b7%a5%e4%bd%9c" aria-label="标题锚点"&gt;&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;在进行操作前，我们需要做一些准备工作，确保不会出现数据丢失的风险：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;创建备份分支并推送到远程：&lt;/strong&gt; 在特性分支 &lt;code&gt;feat&lt;/code&gt; 上创建一个备份分支，并推送到远程仓库，以确保操作过程中的数据安全。&lt;/p&gt;
&lt;div class="td-code td-code--untitled" id="td-code-603fedb9-fence-1" data-td-code data-td-code-auto-id
data-td-language="bash" data-td-line-count="3"&gt;
&lt;div class="td-code__viewport" id="td-code-603fedb9-fence-1-viewport" data-td-code-viewport&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git checkout feat
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git checkout -b feat-backup
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git push origin feat-backup&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;这样，即使后续操作中出现意外，我们依然可以通过 &lt;code&gt;feat-backup&lt;/code&gt; 分支恢复数据。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="步骤详解"&gt;步骤详解&lt;a class="td-heading-self-link" href="#%e6%ad%a5%e9%aa%a4%e8%af%a6%e8%a7%a3" aria-label="标题锚点"&gt;&lt;/a&gt;
&lt;/h2&gt;
&lt;h3 id="1-确定变更点"&gt;1. 确定变更点&lt;a class="td-heading-self-link" href="#1-%e7%a1%ae%e5%ae%9a%e5%8f%98%e6%9b%b4%e7%82%b9" aria-label="标题锚点"&gt;&lt;/a&gt;
&lt;/h3&gt;
&lt;p&gt;首先，我们需要找到 &lt;code&gt;feat&lt;/code&gt; 分支和 &lt;code&gt;main&lt;/code&gt; 分支的&lt;strong&gt;最后一个重合点&lt;/strong&gt;（也就是 &lt;code&gt;main&lt;/code&gt; 分支中包含但 &lt;code&gt;feat&lt;/code&gt; 中不包含的最后一个提交）。这样我们就可以清晰地识别出哪些提交属于 &lt;code&gt;feat&lt;/code&gt;，而哪些提交是混杂进来的。&lt;/p&gt;
&lt;h4 id="如何找到变更点"&gt;如何找到变更点？&lt;a class="td-heading-self-link" href="#%e5%a6%82%e4%bd%95%e6%89%be%e5%88%b0%e5%8f%98%e6%9b%b4%e7%82%b9" aria-label="标题锚点"&gt;&lt;/a&gt;
&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;使用以下命令，找出 &lt;code&gt;feat&lt;/code&gt; 分支中第一个与 &lt;code&gt;main&lt;/code&gt; 分支分叉的提交：&lt;/p&gt;
&lt;div class="td-code td-code--untitled" id="td-code-603fedb9-fence-2" data-td-code data-td-code-auto-id
data-td-language="bash" data-td-line-count="1"&gt;
&lt;div class="td-code__viewport" id="td-code-603fedb9-fence-2-viewport" data-td-code-viewport&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git log --oneline main...feat --reverse --pretty&lt;span class="o"&gt;=&lt;/span&gt;format:&lt;span class="s2"&gt;&amp;#34;%H&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; head -n &lt;span class="m"&gt;1&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;此命令将列出 &lt;code&gt;feat&lt;/code&gt; 分支中所有提交的哈希值（&lt;code&gt;commit hash&lt;/code&gt;），并按照时间顺序排列，其中 &lt;code&gt;head -n 1&lt;/code&gt; 取出第一个分叉点的哈希值，记为 &lt;code&gt;{first_diverge_commit}&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;然后使用以下命令查找 &lt;code&gt;main&lt;/code&gt; 与 &lt;code&gt;feat&lt;/code&gt; 最后一个重合点的哈希值：&lt;/p&gt;
&lt;div class="td-code td-code--untitled" id="td-code-603fedb9-fence-3" data-td-code data-td-code-auto-id
data-td-language="bash" data-td-line-count="1"&gt;
&lt;div class="td-code__viewport" id="td-code-603fedb9-fence-3-viewport" data-td-code-viewport&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git rev-list --parents -n &lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;first_diverge_commit&lt;span class="o"&gt;}&lt;/span&gt; --reverse &lt;span class="p"&gt;|&lt;/span&gt; head -n &lt;span class="m"&gt;1&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;取命令结果的&lt;strong&gt;右边部分&lt;/strong&gt;，这就是 &lt;code&gt;main&lt;/code&gt; 和 &lt;code&gt;feat&lt;/code&gt; 的最后重合点，记为 &lt;code&gt;{last_share_commit}&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="2-计算差异patch"&gt;2. 计算差异（Patch）&lt;a class="td-heading-self-link" href="#2-%e8%ae%a1%e7%ae%97%e5%b7%ae%e5%bc%82patch" aria-label="标题锚点"&gt;&lt;/a&gt;
&lt;/h3&gt;
&lt;p&gt;现在我们已经知道了 &lt;code&gt;main&lt;/code&gt; 分支和 &lt;code&gt;feat&lt;/code&gt; 分支的分叉点和最后重合点，我们可以提取出 &lt;code&gt;feat&lt;/code&gt; 中相对于 &lt;code&gt;main&lt;/code&gt; 的所有变更。&lt;/p&gt;
&lt;p&gt;使用 &lt;code&gt;git diff&lt;/code&gt; 生成差异文件：&lt;/p&gt;
&lt;div class="td-code td-code--untitled" id="td-code-603fedb9-fence-4" data-td-code data-td-code-auto-id
data-td-language="bash" data-td-line-count="1"&gt;
&lt;div class="td-code__viewport" id="td-code-603fedb9-fence-4-viewport" data-td-code-viewport&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git diff main feat &amp;gt; ~/my_patch&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;注意：&lt;/strong&gt; 请将 &lt;code&gt;patch&lt;/code&gt; 文件存放在仓库目录外，例如 &lt;code&gt;~/my_patch&lt;/code&gt;，因为后续执行 &lt;code&gt;git reset&lt;/code&gt; 时会重置仓库目录内的所有文件，导致 &lt;code&gt;patch&lt;/code&gt; 文件丢失。&lt;/p&gt;
&lt;h3 id="3-回溯到最后重合点"&gt;3. 回溯到最后重合点&lt;a class="td-heading-self-link" href="#3-%e5%9b%9e%e6%ba%af%e5%88%b0%e6%9c%80%e5%90%8e%e9%87%8d%e5%90%88%e7%82%b9" aria-label="标题锚点"&gt;&lt;/a&gt;
&lt;/h3&gt;
&lt;p&gt;接下来，我们要将当前 &lt;code&gt;feat&lt;/code&gt; 分支的状态回退到 &lt;code&gt;main&lt;/code&gt; 分支的最后重合点。&lt;/p&gt;
&lt;div class="td-code td-code--untitled" id="td-code-603fedb9-fence-5" data-td-code data-td-code-auto-id
data-td-language="bash" data-td-line-count="1"&gt;
&lt;div class="td-code__viewport" id="td-code-603fedb9-fence-5-viewport" data-td-code-viewport&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git reset --hard &lt;span class="o"&gt;{&lt;/span&gt;last_share_commit&lt;span class="o"&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;该命令将 &lt;code&gt;feat&lt;/code&gt; 分支的状态重置到 &lt;code&gt;{last_share_commit}&lt;/code&gt; 提交。&lt;strong&gt;注意：&lt;/strong&gt; &lt;code&gt;reset --hard&lt;/code&gt; 会丢失所有当前分支的更改，因此确保之前的 &lt;code&gt;patch&lt;/code&gt; 文件已备份。&lt;/p&gt;
&lt;h3 id="4-快进主干fast-forward-merge"&gt;4. 快进主干（Fast-Forward Merge）&lt;a class="td-heading-self-link" href="#4-%e5%bf%ab%e8%bf%9b%e4%b8%bb%e5%b9%b2fast-forward-merge" aria-label="标题锚点"&gt;&lt;/a&gt;
&lt;/h3&gt;
&lt;p&gt;现在，我们需要让 &lt;code&gt;feat&lt;/code&gt; 分支快进（fast-forward）到主干 &lt;code&gt;main&lt;/code&gt; 的最新状态。&lt;/p&gt;
&lt;div class="td-code td-code--untitled" id="td-code-603fedb9-fence-6" data-td-code data-td-code-auto-id
data-td-language="bash" data-td-line-count="1"&gt;
&lt;div class="td-code__viewport" id="td-code-603fedb9-fence-6-viewport" data-td-code-viewport&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git merge main --ff-only&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;code&gt;--ff-only&lt;/code&gt; 参数表示如果不能进行快进合并，则不会合并。这一步确保 &lt;code&gt;feat&lt;/code&gt; 分支的历史记录与 &lt;code&gt;main&lt;/code&gt; 分支保持一致。&lt;/p&gt;
&lt;h3 id="5-应用补丁apply-patch"&gt;5. 应用补丁（Apply Patch）&lt;a class="td-heading-self-link" href="#5-%e5%ba%94%e7%94%a8%e8%a1%a5%e4%b8%81apply-patch" aria-label="标题锚点"&gt;&lt;/a&gt;
&lt;/h3&gt;
&lt;p&gt;回退和快进操作完成后，我们将 &lt;code&gt;feat&lt;/code&gt; 分支上原本存在的所有提交变更（即 &lt;code&gt;patch&lt;/code&gt; 文件）重新应用到当前分支。&lt;/p&gt;
&lt;div class="td-code td-code--untitled" id="td-code-603fedb9-fence-7" data-td-code data-td-code-auto-id
data-td-language="bash" data-td-line-count="1"&gt;
&lt;div class="td-code__viewport" id="td-code-603fedb9-fence-7-viewport" data-td-code-viewport&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git apply ~/my_patch&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;h3 id="6-提交变更并推送"&gt;6. 提交变更并推送&lt;a class="td-heading-self-link" href="#6-%e6%8f%90%e4%ba%a4%e5%8f%98%e6%9b%b4%e5%b9%b6%e6%8e%a8%e9%80%81" aria-label="标题锚点"&gt;&lt;/a&gt;
&lt;/h3&gt;
&lt;p&gt;现在，我们可以创建一个新的提交，将 &lt;code&gt;feat&lt;/code&gt; 分支的所有变更整合到一个提交中。&lt;/p&gt;
&lt;div class="td-code td-code--untitled" id="td-code-603fedb9-fence-8" data-td-code data-td-code-auto-id
data-td-language="bash" data-td-line-count="2"&gt;
&lt;div class="td-code__viewport" id="td-code-603fedb9-fence-8-viewport" data-td-code-viewport&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git add .
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git commit -m &lt;span class="s2"&gt;&amp;#34;Squashed all feature changes into a single commit.&amp;#34;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;接着，将 &lt;code&gt;feat&lt;/code&gt; 分支强制推送到远端，以确保远程仓库与本地分支一致：&lt;/p&gt;
&lt;div class="td-code td-code--untitled" id="td-code-603fedb9-fence-9" data-td-code data-td-code-auto-id
data-td-language="bash" data-td-line-count="1"&gt;
&lt;div class="td-code__viewport" id="td-code-603fedb9-fence-9-viewport" data-td-code-viewport&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git push origin feat --force&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="总结"&gt;总结&lt;a class="td-heading-self-link" href="#%e6%80%bb%e7%bb%93" aria-label="标题锚点"&gt;&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;通过上述操作，我们成功地将 &lt;code&gt;feat&lt;/code&gt; 分支的所有提交整合成了一个提交点，并且与主干保持了清晰的历史记录。完整步骤如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;找出特性分支和主干的分叉点与重合点。&lt;/li&gt;
&lt;li&gt;生成 &lt;code&gt;patch&lt;/code&gt; 文件保存变更。&lt;/li&gt;
&lt;li&gt;回溯到重合点并进行快进合并。&lt;/li&gt;
&lt;li&gt;应用 &lt;code&gt;patch&lt;/code&gt; 文件，合并所有更改。&lt;/li&gt;
&lt;li&gt;提交合并后的变更，并推送到远端。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;通过这种手动 &lt;code&gt;squash&lt;/code&gt; 方法，你可以灵活地调整提交历史，确保代码库的整洁度和可读性。&lt;/p&gt;
&lt;h2 id="勘误"&gt;勘误&lt;a class="td-heading-self-link" href="#%e5%8b%98%e8%af%af" aria-label="标题锚点"&gt;&lt;/a&gt;
&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;感谢评论区 @Certseeds 的指正，第 2 步的 diff 计算指令错误地将 last_share_commit 作为了 diff 开始点，实际上应该为 main（主干分支）；原文已修复。&lt;/li&gt;
&lt;/ul&gt;</description></item></channel></rss>