如何手动 squash
- Git
最近帮解决了一个因为提交流程不规范导致的诡异 Git 分支问题,特此记录下,以备后用。
背景:主干分支 main,特性分支 feat,在特性分支上开发特性的时候,多次合入了主干分支(仅快进,没有合并冲突);模拟的 Git 历史如下图所示,其中 m*
是主干提交,f*
是特性分支提交
* 5fb94b2 (HEAD -> main) m4
| * 83c6299 (feat) f3
| * ff5c5af Merge branch 'main' into feat
| |\
| |/
|/|
* | 1698c3c m3
* | 05199a8 m2
| * 68a7e1d f2
| * eb5b169 f1
|/
* ae79b7c m1
问题:如何把特性分支上的所有提交合并为一个提交?(类似于 squash 的 Merge 策略,但是手动做到这一点)
思路:找到主干和特性分支的第一个分叉点,以此为基准生成 patch,然后在一个新分支上 apply patch 得到一个纯净的 commit。
(非人工写作提示:以下是 LLM 根据我自己的笔记生成的内容。欢迎将反馈贴在评论区,这将决定我以后是否会更积极地使用 LLM 进行创作。)
在日常开发中,我们经常会遇到这样的问题:由于开发流程、代码审查或其他原因,特性分支(Feature Branch)和主干(Main Branch)上的提交记录混杂在一起,难以管理。为了让代码历史更为清晰、整洁,我们通常需要手动进行 squash
操作,将特性分支上的多次提交合并成一个提交点。
这篇文章将带你一步步了解如何手动进行 squash
操作,并确保不丢失任何数据。这种方法适用于已经有部分提交合并进主干,并且历史记录较为复杂的场景。
场景问题
假设你正在开发一个新功能,但在开发过程中,特性分支 feat
和主干分支 main
上的提交混杂在一起。这样一来,不仅使代码历史难以追溯,还会影响代码审查和后续维护。因此,我们希望将 feat
分支上的所有提交整合成一个提交点,并保持代码历史的清晰度。
前置准备工作
在进行操作前,我们需要做一些准备工作,确保不会出现数据丢失的风险:
-
创建备份分支并推送到远程: 在特性分支
feat
上创建一个备份分支,并推送到远程仓库,以确保操作过程中的数据安全。git checkout feat git checkout -b feat-backup git push origin feat-backup
这样,即使后续操作中出现意外,我们依然可以通过
feat-backup
分支恢复数据。
步骤详解
1. 确定变更点
首先,我们需要找到 feat
分支和 main
分支的最后一个重合点(也就是 main
分支中包含但 feat
中不包含的最后一个提交)。这样我们就可以清晰地识别出哪些提交属于 feat
,而哪些提交是混杂进来的。
如何找到变更点?
-
使用以下命令,找出
feat
分支中第一个与main
分支分叉的提交:git log --oneline main...feat --reverse --pretty=format:"%H" | head -n 1
此命令将列出
feat
分支中所有提交的哈希值(commit hash
),并按照时间顺序排列,其中head -n 1
取出第一个分叉点的哈希值,记为{first_diverge_commit}
。 -
然后使用以下命令查找
main
与feat
最后一个重合点的哈希值:git rev-list --parents -n 1 {first_diverge_commit} --reverse | head -n 1
取命令结果的右边部分,这就是
main
和feat
的最后重合点,记为{last_share_commit}
。
2. 计算差异(Patch)
现在我们已经知道了 main
分支和 feat
分支的分叉点和最后重合点,我们可以提取出 feat
中相对于 main
的所有变更。
使用 git diff
生成差异文件:
git diff main feat > ~/my_patch
注意: 请将 patch
文件存放在仓库目录外,例如 ~/my_patch
,因为后续执行 git reset
时会重置仓库目录内的所有文件,导致 patch
文件丢失。
3. 回溯到最后重合点
接下来,我们要将当前 feat
分支的状态回退到 main
分支的最后重合点。
git reset --hard {last_share_commit}
该命令将 feat
分支的状态重置到 {last_share_commit}
提交。注意: reset --hard
会丢失所有当前分支的更改,因此确保之前的 patch
文件已备份。
4. 快进主干(Fast-Forward Merge)
现在,我们需要让 feat
分支快进(fast-forward)到主干 main
的最新状态。
git merge main --ff-only
--ff-only
参数表示如果不能进行快进合并,则不会合并。这一步确保 feat
分支的历史记录与 main
分支保持一致。
5. 应用补丁(Apply Patch)
回退和快进操作完成后,我们将 feat
分支上原本存在的所有提交变更(即 patch
文件)重新应用到当前分支。
git apply ~/my_patch
6. 提交变更并推送
现在,我们可以创建一个新的提交,将 feat
分支的所有变更整合到一个提交中。
git add .
git commit -m "Squashed all feature changes into a single commit."
接着,将 feat
分支强制推送到远端,以确保远程仓库与本地分支一致:
git push origin feat --force
总结
通过上述操作,我们成功地将 feat
分支的所有提交整合成了一个提交点,并且与主干保持了清晰的历史记录。完整步骤如下:
- 找出特性分支和主干的分叉点与重合点。
- 生成
patch
文件保存变更。 - 回溯到重合点并进行快进合并。
- 应用
patch
文件,合并所有更改。 - 提交合并后的变更,并推送到远端。
通过这种手动 squash
方法,你可以灵活地调整提交历史,确保代码库的整洁度和可读性。
勘误
- 感谢评论区 @Certseeds 的指正,第 2 步的 diff 计算指令错误地将 last_share_commit 作为了 diff 开始点,实际上应该为 main(主干分支);原文已修复。