跳至主要内容

Git 压缩提交(Squash Commits):示例指南

了解如何使用交互式变基在分支上压缩提交,帮助保持整洁有序的提交历史。
· 7分钟 阅读

使用 AI 探索

“早提交、勤提交”(Commit early, commit often)是在使用 Git 进行软件开发时广为流传的箴言。这样做可确保每项更改都有良好的记录,促进协作,并更容易追踪项目的演进。不过,这也可能导致提交数量过多。

这正是压缩提交(squash commits)的重要性所在。压缩提交是将多个提交合并为一个完整统一的提交的过程。

例如,假设我们在开发一个实现登录表单的功能,并创建了以下四个提交:

Git 提交历史示例

功能完成后,从整个项目的角度看,这些提交过于细碎。我们未来并不需要知道开发中遇到了一个随后修复的 bug。为确保主分支历史干净,我们将这些提交压缩为一个提交:

Git 提交压缩示例

如何在 Git 中压缩提交:交互式变基

最常见的压缩提交方法是使用交互式变基。我们通过以下命令启动:

git rebase -i HEAD~<number_of_commits>

将 <number_of_commits> 替换为要压缩的提交数量。

在我们的例子中有四个提交,因此命令为:

git rebase -i HEAD~4

执行该命令会打开一个交互式命令行编辑器:

运行 git rebase -i 后的 CLI 编辑器

上方区域显示提交列表,下方包含关于如何压缩提交的注释。

我们看到有四个提交。对于每个提交,都需要决定执行哪条命令。我们关注 pick(p)和 squash(s)命令。要将这四个提交压缩为一个提交,可以 pick 第一个提交,并将其余三个设置为 squash。

通过修改每个提交前的文本来应用命令,具体做法是将第二、第三和第四个提交前的 pick 改为 s 或 squash。要进行这些编辑,需要在命令行文本编辑器中按下键盘上的 i 键进入“INSERT”模式:

在 CLI 编辑器中进入插入模式

按下 i 后,底部会出现 -- INSERT -- 文本,表示已进入插入模式。现在可以用方向键移动光标、删除字符,并像在常规文本编辑器中一样输入:

与 CLI 文本编辑器交互

完成修改后,需要按键盘上的 Esc 键退出插入模式。下一步是保存更改并退出编辑器。为此,先按下 : 键,告知编辑器我们要执行命令:

在 CLI 文本编辑器中输入命令

现在在编辑器底部可以看到分号 :,提示我们输入命令。保存更改使用命令 w(write 的缩写)。关闭编辑器使用 q(quit 的缩写)。这两个命令可以合并为 wq 一起输入:

如何保存并退出 CLI 文本编辑器

按下 Enter 执行命令。此操作会关闭当前编辑器并打开一个新编辑器,用于为新压缩的提交输入提交消息。编辑器会显示一个默认消息,包含我们正要压缩的四个提交的消息:

编辑压缩提交的提交消息

建议修改该消息,使其准确反映这些合并提交所实现的更改——毕竟,压缩的目的就是保持历史清晰、易读。

要与编辑器交互并编辑消息,再次按下 i 进入编辑模式,按需修改消息。

压缩提交消息示例

本例中,我们将提交消息替换为 “Implement login form”。退出编辑模式请按 Esc,然后按 : 输入 wq 命令并按 Enter 保存更改。

如何查看提交历史

通常,回忆完整的提交历史并不容易。要查看提交历史,可以使用 git log 命令。在上述示例中,执行压缩之前运行 git log 会显示:

压缩前的 Git log 提交历史

使用上下方向键浏览提交列表。按 q 退出。

我们可以使用 git log 来确认压缩是否成功。压缩后执行该命令,将只显示带有新消息的单个提交:

压缩后的 Git log 提交历史

推送已压缩的提交

上述命令只会作用于本地仓库。要更新远程仓库,需要推送更改。然而,由于我们更改了提交历史,需要使用 --force 选项进行强制推送:

git push --force origin feature/login-form

强制推送会覆盖远程分支的提交历史,并可能干扰在该分支上工作的其他人。进行此操作前最好与团队沟通。

一种更安全、降低干扰协作者风险的强制推送方式,是使用 --force-with-lease 选项:

git push --force-with-lease origin feature/login-form

该选项可确保仅在自上次 fetch 或 pull 后远程分支未被更新的情况下才进行强制推送。

压缩特定提交

设想我们有五个提交:

包含五个提交的 Git log 示例

假设我们想保留提交 1、2 和 5,并将提交 3 与 4 压缩。

压缩任意提交

在交互式变基中,被标记为压缩的提交会与其直接前一个提交合并。在本例中,意味着我们希望将 Commit4 压缩并合并到 Commit3 中。

为此,我们必须启动一个覆盖这两个提交的交互式变基。本例中只需包含最近的三个提交,因此使用命令:

git rebase -i HEAD~3

然后将 Commit4 设置为 s,使其与 Commit3 一起压缩:

压缩两个提交示例

执行该操作并列出提交后,我们会看到提交 3 和 4 已被压缩在一起,而其他提交保持不变。

压缩后的 Git log

从特定提交开始压缩

在命令 git rebase -i HEAD~3 中,HEAD 表示最新提交的简写。~3 语法用于指定某个提交的祖先。例如,HEAD~1 指的是 HEAD 提交的父提交。

~ 符号示意

在交互式变基中,被考虑的提交包括一直回溯到命令中指定提交之前的所有祖先提交。请注意,被指定的那个提交本身不包含在内:

交互式变基中包含哪些提交

我们也可以不使用 HEAD,而是直接指定提交哈希。例如,Commit2 的哈希为 dbf3cc118d6d7c08ef9c4a326b26dbb1e3fe9ddf,因此命令:

git rebase -i dbf3cc118d6d7c08ef9c4a326b26dbb1e3fe9ddf

将会从 Commit2 之后的提交开始变基。因此,如果我们希望从特定提交开始变基并将该提交也包含在内,可以使用:

git rebase -i <commit-hash>~1

压缩提交时解决冲突

当我们压缩提交时,会将多个提交的更改合并为一个提交,如果更改存在重叠或差异较大,就可能引发冲突。以下是一些常见的冲突场景:

  1. 更改重叠:如果被压缩的两个或多个提交修改了同一文件的相同行或相邻行,Git 可能无法自动合并这些更改。
  2. 更改状态不同:如果一个提交新增了某段代码,而另一个提交修改或删除了同一段代码,压缩这些提交时就会产生需要解决的冲突。
  3. 重命名并修改:如果某个提交重命名了文件,而后续提交仍对旧文件名进行修改,那么在压缩这些提交时可能会让 Git 产生混淆,从而引发冲突。
  4. 二进制文件的更改:二进制文件无法通过基于文本的 diff 工具良好地合并。如果多个提交更改了同一个二进制文件,并尝试压缩它们,就可能发生冲突,因为 Git 无法自动协调这些更改。
  5. 历史复杂:如果这些提交的历史较为复杂,包含多次合并、分支或变基,压缩它们可能会因为历史的非线性而导致冲突。

在压缩过程中,Git 会尝试逐个应用更改。如果过程中遇到冲突,它会暂停并允许我们进行解决。

冲突会用冲突标记 <<<<<< 和 >>>>>> 标出。处理冲突需要打开相关文件,手动选择要保留的代码片段并完成合并。

解决冲突后,需要使用 git add 将已解决的文件暂存起来。随后使用以下命令继续变基:

git rebase --continue

关于 Git 冲突的更多内容,请查看这篇教程:如何解决 Git 合并冲突。

使用变基以外的压缩替代方案

git merge --squash 是与 git rebase -i 等效的另一种将多个提交合并为单个提交的方法。当我们希望将某个分支的更改合并到主分支,同时将所有单独提交压缩成一次提交时,该命令尤为有用。以下是使用 git merge 进行压缩的概览:

  1. 切换到要合并更改的目标分支。
  2. 执行命令 git merge --squash <branch-name>,将 <branch-name 替换为分支名称。
  3. 使用 git commit 提交更改,创建一个代表该功能分支所有更改的单个提交。

例如,假设我们希望将 feature/login-form 分支的更改以单个提交的形式合入 main:

git checkout main
git merge --squash feature-branch
git commit -m "Implement login form"

与 git rebase -i 相比,该方法的局限性包括:

  • 控制粒度: 对单个提交的控制较少。使用变基可以选择要合并的提交,而合并会将所有更改强制合并为一个提交。
  • 中间历史: 使用合并时,功能分支的单个提交历史在主分支中会丢失。这会让追踪功能开发过程中的增量更改变得更困难。
  • 提交前审查: 由于会将所有更改作为一个变更集进行暂存,我们无法像交互式变基那样在压缩前对每个提交逐一审查或测试。

结论

在开发流程中频繁、小步提交有助于协作与清晰的文档记录,但也可能使项目历史变得杂乱。压缩提交在两者之间取得平衡:保留关键里程碑,同时去除微小迭代带来的噪音。

想要进一步学习 Git,推荐参考以下资源:

François Aubry

作者

François Aubry

全栈工程师,CheapGPT 创始人。我一直热爱教学。从学生时代起,我就积极寻找机会为同学提供辅导与帮助。这份热情促使我攻读博士学位,并在此期间担任助教,以支持我的学术追求。那些年里,我在传统课堂环境中收获了极大的成就感,既建立联系也促进学习。随着在线学习平台的兴起,我也看到了数字教育的变革潜力。事实上,我曾参与我们大学一款此类平台的建设。我致力于将传统教学理念与创新的数字方法相融合。我的热情在于打造既有吸引力、信息量充足,又能让当代学习者轻松获取的课程。

主题