devops-exercises 实战:Git Squashing Commits 提交压缩练习全解析
devops-exercises 实战:Git Squashing Commits 提交压缩练习全解析
导读
本文以 devops-exercises 仓库 Git 主题下的 Squashing Commits 练习 为核心,系统讲解如何将多个 Git 提交合并(squash)为一个提交。通过"创建两个提交 → 验证 → 交互式变基压缩"的完整实操流程,你将掌握 git rebase -i 的核心用法、pick 与 squash 的配合方式,并理解提交压缩在保持仓库历史整洁、便于代码评审与问题追踪中的实际价值。读完本文,你不仅能独立完成该练习,还能应对其衍生边界情况(如根提交无法变基的报错处理)。
练习目标与前置知识
本次练习要解决什么问题
官方练习 squashing_commits.md 的目标非常简单直接:学会如何压缩(squash)提交。它要求你在一个 Git 仓库中:
- 新建一个内容为 "Mario" 的文件并创建一次提交;
- 修改该文件内容为 "Mario & Luigi" 并创建第二次提交;
- 验证你确实拥有两个独立的提交;
- 将最近两次提交压缩为一个提交。
这本质上是开发者日常工作中最常见的 Git 历史整理需求:当你在一个功能分支上留下了大量"小碎步"式的提交(如 fix typo、remove a character),合并进主干之前,往往希望把它们整理成逻辑完整的少数几个提交,让历史更清晰、更易回溯。
前置条件:先会创建提交
该练习假设你已经具备最基本的 Git 提交能力。如果你尚未接触过提交操作,建议先完成仓库中的 commit_01.md 练习,其参考答案 commit_01_solution.md 展示了最基础的提交链路:
mkdir my_repo && cd my_repo
git init
echo "hello_commit" > file
git add file
git commit -a -m "It's my first commit. Exciting!"
git log
可以看到,一次标准提交由三个环节构成:git init 初始化仓库 → git add/git commit -a 将改动纳入版本控制 → git log 验证提交被记录。Squashing Commits 练习正是建立在这一链路之上:先制造出两个提交,再通过变基将其合并。
分步完成 Squashing Commits 练习
官方参考答案位于 topics/git/solutions/squashing_commits.md,下面按步骤完整展开并补充命令说明。
第一步:创建文件并提交第一个版本
echo "Mario" > new_file
git add new_file
git commit -m "New file"
echo "Mario" > new_file:在工作目录创建名为new_file的文件,内容为Mario;git add new_file:把该文件加入暂存区(staging area / index),这一步至关重要——Git 只会提交被显式跟踪的内容;git commit -m "New file":生成第一个提交,提交信息为 "New file"。
第二步:修改内容并创建第二个提交
echo "Mario & Luigi" > new_file
git commit -a -m "Added Luigi"
这里使用了 git commit -a 的便捷特性:-a 参数会自动把所有已被 Git 跟踪(tracked)且发生改动的文件加入暂存并提交,省去了先执行 git add 的步骤。因为 new_file 在第一步已经被跟踪,所以这一步直接修改内容后即可提交。提交信息为 "Added Luigi"。
第三步:验证存在两个独立提交
git log
运行 git log 查看提交历史,输出类似:
commit 4016808 (HEAD)
Author: Your Name <you@example.com>
Date: ...
Added Luigi
commit 5412076
Author: Your Name <you@example.com>
Date: ...
New file
此时你应该看到两条提交记录:最近一条是 Added Luigi,最早一条是 New file。验证这一步是练习的关键——只有确认存在两个独立提交,后续压缩才有意义,也才能直观对比压缩前后的历史变化。
第四步:使用交互式变基压缩最近两个提交
git rebase -i HEAD~2
HEAD~2 表示"当前 HEAD 之前的第 2 个提交",也就是本次要重写的提交范围。执行后 Git 会打开一个文本编辑器,显示类似如下的待操作列表:
pick 5412076 New file
pick 4016808 Added Luigi
Git 默认对列表中的每个提交使用 pick 指令(保留该提交及其信息)。要压缩提交,只需把除第一条以外的 pick 改为 squash:
pick 5412076 New file
squash 4016808 Added Luigi
随后保存并退出编辑器,Git 会再次打开编辑器让你为合并后的新提交提供提交信息(默认会汇总两条原始提交的信息)。确认后,变基完成,两个提交被合并为一个。
指令含义速查表:
| 指令 | 作用 |
|---|---|
pick |
保留该提交(默认指令,按原样应用到新历史中) |
squash |
将该提交的内容合并到其上一个提交中,并允许合并提交信息 |
fixup |
类似 squash,但直接丢弃该提交的提交信息(常用于合并"笔误修正"类提交) |
reword |
保留提交但重新编辑提交信息 |
edit |
保留提交并在应用时停下来,允许修改内容 |
drop |
删除该提交 |
压缩完成后再次运行 git log,你应该只会看到一条提交,且其内容同时包含 "Mario" 与 "Mario & Luigi" 两个版本的变更。
边界情况:HEAD~2 报错怎么办
官方参考答案特别标注了一个高频报错场景:如果运行 git rebase -i HEAD~2 返回类似 fatal: invalid upstream 'HEAD~2' 的错误,通常意味着你的第二个提交实际上就是仓库的根提交(root commit),在它之前不存在任何父提交,因此 HEAD~2 指向了不存在的对象。解决方案有两种:
- 使用
git rebase -i --root:允许重写根提交本身,从而可以对全部历史进行压缩; - 先创建一个初始提交:让仓库中先存在一个垫底的提交,使
HEAD~2能指向两个有效的、待压缩的提交。
这一提示是练习文档为实操者预留的"防坑"细节,在实际练习中非常值得注意。
练习后的思考题:为什么需要压缩提交
完成练习后,官方文档要求回答两个问题,这里给出完整解答。
压缩提交的理由是什么?
压缩提交的核心价值是让历史变得更干净(cleaner)、更易追踪(easier to track)。 具体来说:
- 消除无意义的小提交:开发过程中经常会出现
removed a character、fix typo这类零碎提交,它们对理解代码演进没有价值,反而增加噪音; - 形成语义完整的功能单元:把"实现功能"+"修 bug"+"补充注释"等零散提交合并成一个逻辑完整的提交,使每次提交代表一个可独立理解、可单独回滚的功能点;
- 降低 Code Review 成本:评审者面对的是少而精的提交,而非一串碎片化改动,更容易定位每个变更的意图;
- 配合主干合并策略:在将功能分支合并回主干(如 main)前压缩提交,可以让主干历史保持线性、简洁。
仓库 Git 主题页 topics/git/README.md 中的一道相关思考题也从侧面印证了提交质量的关注点:.gitignore 的作用是确保某些文件永远不被跟踪,git status 用于理解工作目录与暂存区的跟踪状态——这些都是保持"提交干净"的配套手段。压缩提交与它们共同构成了 Git 历史管理的完整实践。
是否可能压缩超过 2 个提交?
可以。 git rebase -i HEAD~N 中的 N 可以取任意大于 1 的数值,表示重写最近 N 个提交;在待操作列表中,你可以把第 2 条及之后的任意条目的指令改为 squash,将它们全部合并到列表第一条提交中。例如:
pick 1111111 Commit A
squash 2222222 Commit B
squash 3333333 Commit C
即可把三个提交压缩为一个。若只想保留其中某条提交的提交信息,其余使用 squash;若只想丢弃部分提交信息,可使用 fixup。需要注意的是:压缩超过 2 个提交时,合并后的提交内容将包含所有这些提交的累积变更,提交信息编辑器里也会列出所有被压缩提交的原始信息供你整合。
深入理解:提交压缩背后的 Git 基础概念
为了让练习"知其所以然",结合 topics/git/README.md 中的基础知识,梳理与提交压缩直接相关的几个核心概念。
Git 的三个状态区域
Git 目录(.git)中存储着项目的元数据和对象数据库(提交对象、树对象、blob 对象等),这是 Git 的核心;工作目录(working directory)是从 Git 目录中检出到磁盘上的文件版本,供你修改使用;暂存区(staging area / index)则记录了下一个提交将包含哪些内容。一次提交的本质是:把暂存区的内容快照固化成一个 commit 对象。理解这一点就能明白,为什么 git add 是提交前必不可少的一步,以及为什么 git commit -a 可以跳过显式 add(因为它隐含完成了"把已跟踪文件的改动加入暂存"的动作)。
git log 验证提交被记录
练习第三步用 git log 验证提交数量。git log 会沿提交对象的父指针(parent)反向遍历历史,从而展示完整的提交链。在压缩提交前后各执行一次 git log,可以直观看到历史从两条变为一条——这正是压缩操作在对象层面的结果:变基重建了提交对象,使后一个提交成为前一个提交的"合并结果",而非其子提交。
与 git reset、git revert 的差异
仓库 README 的 Git 问答部分提到:git revert 会创建一个新的提交来撤销上一次提交的改动(不修改历史);而 git reset 根据用法不同,可能修改暂存区,或改变分支头(branch head)指向的提交。git rebase -i 的提交压缩属于"重写历史"类操作,与 revert(追加式撤销)有本质区别——压缩后的提交 SHA 会发生变化,且会改写历史,因此只应在未推送到共享远程分支(或与团队达成一致)时使用,这一点在使用练习时需要特别留意。
在 devops-exercises 中完成本练习
本练习是 devops-exercises 仓库 Git 主题的三个基础练习之一,与该主题下的其他练习形成递进关系:
- commit_01.md:学会创建第一次提交,参考解答见 commit_01_solution.md;
- branch_01.md:学会基于已有提交创建分支并验证提交只出现在特定分支,参考解答见 branch_01_solution.md;
- squashing_commits.md:即本文主体,学会压缩提交,参考解答见 squashing_commits.md 答案。
建议按"提交 → 分支 → 压缩"的顺序依次练习:先掌握提交与分支的基本操作,再在分支上进行多次提交、最后压缩合并,这正好复现真实开发中"功能分支整理历史再合并"的完整工作流。该主题的所有练习入口与更多 Git 问答都汇总在 topics/git/README.md 中,包括 git pull 与 git fetch 的区别、合并冲突的解决方式、git reset 与 git revert 的差异等高频面试题,可与本练习搭配复习。
小结
通过本文,你完成了 devops-exercises 的 Squashing Commits 练习全流程:从创建两个提交、用 git log 验证,到用 git rebase -i HEAD~2 将 pick 改为 squash 完成压缩,并掌握了根提交场景下 --root 的兜底方案。同时明确了压缩提交的两大价值——历史更干净、变更更易追踪,以及它可以作用于任意数量的提交。将这一技能应用到你的真实开发流程中(尤其是功能分支合并主干前),会让整个团队受益于更清晰、更可回溯的提交历史。