第五章. Git 提交规范指南:Conventional Commits 工程化实践
第五章. Git 提交规范指南:Conventional Commits 工程化实践
Prorise第五章. Git 提交规范指南:Conventional Commits 工程化实践
摘要:在深入理解了暂存区的物理结构与对象存储机制后,本章将彻底摒弃“一把梭”的粗放式提交习惯。我们将深入 Git 的交互式暂存模式,掌握按代码块(Hunk)甚至按行(Line)筛选变更的微操技术,实现真正的原子化提交。同时,我们将建立严格的提交记录管理标准,从 Commit Message 的结构化规范(Conventional Commits),到 Amend 修补历史的底层逻辑,再到空提交(Empty Commit)在自动化流水线中的特殊应用,全方位构建企业级的版本交付能力。
本章学习路径
- 微操暂存:使用
add -p和补丁编辑模式,将混杂的修改拆解为原子化的提交。 - 状态回退:辨析
restore --staged与reset的区别,安全地管理暂存区内容。 - 历史修补:掌握
amend修改提交元数据的方法,并理解其对 Commit ID 的影响。 - 特殊技巧:利用
allow-empty触发 CI 以及--author修正身份的冷门但重要技巧。 - 工程规范:深度解析 Conventional Commits 标准,让提交信息成为可读文档。
5.1. 暂存区的高级操作:交互式筛选实战
在上一章,我们习惯了使用 git add . 一把梭。但在团队协作中,这种粗放的操作往往是事故的根源。为了解决“一次提交包含多个无关修改”的痛点,Git 提供了一把手术刀——交互式暂存(Interactive Staging)。
本节我们将通过一个精心设计的实战,跨越文件的物理边界,在 代码块(Hunk) 甚至 行(Line) 的颗粒度上进行精细化操作。
5.1.1. 核心概念:什么是 Hunk?
在 Git 的视角里,文件并不是最小单位。当 Git 扫描变更时,它会把 连续变化的行 以及它们周围的上下文(Context)聚合在一起,形成一个 Hunk(代码块)。
- 如果两个修改点相隔很远(例如超过 3 行),Git 会自动将它们识别为 两个 Hunk。
- 如果两个修改点挨得很近,Git 会默认将它们合并为 一个 Hunk(我们稍后会学习如何用
s强行拆散它们)。
5.1.2. 环境初始化:构建“混合修改”现场
为了确保实验结果的一致性,我们需要构建一个代码行数足够多、能让 Git 自动识别出分隔的文件。
步骤 1:创建初始状态
请在项目根目录创建 MathUtils.java,并直接复制以下内容(注意中间的空行不能少,这是为了隔离 Hunk):
1 | public class MathUtils { |
提交这个初始版本:
1 | git add MathUtils.java |
步骤 2:制造“脏代码”与“修复”共存的现场
现在,我们模拟真实场景:你修复了 divide 的 Bug(正事),但顺手在 add 里加了调试打印(脏代码)。
请用以下代码 完全覆盖 MathUtils.java:
1 | public class MathUtils { |
此时的目标是:只提交修改点 B,丢弃修改点 A。
5.1.3. 实战一:使用 s (Split) 拆分与筛选
在终端执行交互式命令:
1 | git add -p MathUtils.java |
交互界面深度解析
Git 会扫描文件,并展示第一个差异块。此时底部的提示符 [y,n,q,a,d,s,e,?] 是我们操作的核心。你需要熟练掌握以下关键指令:
| 指令 | 全称 | 含义 | 适用场景 |
|---|---|---|---|
| y | Yes | 同意。将当前 Hunk 加入暂存区。 | 这是我想要提交的代码。 |
| n | No | 拒绝。跳过当前 Hunk,保留在工作区。 | 这是调试代码或未完成的功能。 |
| s | Split | 拆分。将当前大块拆解为更小的块。 | 当 Git 误将两个无关修改合并显示时使用。 |
| e | Edit | 编辑。手动修改补丁内容。 | 修改在同一行,无法拆分时使用(高阶技巧)。 |
| q | Quit | 退出。 | 剩下的都不看了,结束操作。 |
操作流程演示
场景 A:如果 Git 显示 (1/2)
这意味着我们预留的空行起作用了,Git 自动识别出了两个块。
- 看到包含
System.out.println的块时,输入n(拒绝)。 - 看到包含
if (b == 0)的块时,输入y(同意)。
场景 B:如果 Git 显示 (1/1)(我们正常可能不会遇到,但有可能!)
这意味着修改点离得太近,Git 把它们合并了。
- 此时不要急着选 y 或 n,请输入
s并回车。 - Git 会强制根据未修改的行进行切割,然后重新问你。
- 切开后,针对脏代码输入
n,针对修复代码输入y。
验证结果
操作完成后,执行 git status,你会看到神奇的 “双重状态”:
1 | Changes to be committed: |
5.1.4. 实战二:使用 e (Edit) 模式处理行内混合
s 命令虽然好用,但它有一个致命局限:如果修改在同一行,或者紧挨着,它就切不开了。
这时,我们需要用到 Git 的终极武器:补丁编辑(Patch Editing)。
场景模拟
创建一个配置文件 config.properties 并提交:
1 | timeout=1000 |
1 | git add config.properties |
现在,我们将它修改为:
1 | requestTimeout=5000 |
这里发生了两个变化:
- 重构:
timeout->requestTimeout - 改值:
1000->5000
目标:先提交重构(改名),但不提交改值。
操作指引
- 执行
git add -p config.properties。 - Git 会显示:
1
2-timeout=1000
+requestTimeout=5000 - 因为变化在同一行,输入
s无效。请输入e(Edit)。
手动伪造补丁
Git 会打开编辑器,展示 Diff 内容。请注意,你现在编辑的不是文件,而是即将生效的“补丁”。
将编辑器中的内容修改为:
1 | -timeout=1000 |
- 我们保留
-行(旧代码)。 - 我们手动把
+行里的5000改回1000。这相当于告诉 Git:“我这次只改了变量名,没改值”。
保存关闭后,执行 git diff --cached,你会发现暂存区里确实只存入了变量名的修改。
5.1.5. 撤销暂存:git restore --staged 的反向操作
不仅 git add 可以交互式,撤销也可以。
如果你不小心把一堆文件加到了暂存区,想把其中几个特定的 Hunk 拿出来(比如发现里面混了密码),但这比较少用,可以使用:
1 | git restore --staged -p |
此时的逻辑是 反向 的:
- 输入
y:表示 “是的,我要把这个块踢出暂存区”(Unstage)。 - 输入
n:表示 “不,把这个块留在暂存区里”(Keep staged)。
5.2. 提交修补与特殊元数据修正(非重要)
在完成暂存区的精细控制后,我们执行了 git commit。但人非圣贤,提交之后我们可能会发现:
- Commit Message 写了个错别字。
- 少提交了一个配置文件。
- 用了错误的个人邮箱提交了公司代码。
如果创建新的提交来修正这些错误,会导致历史记录变得脏乱差(Dirty History)。Git 提供了 amend 机制,让我们能够“时光倒流”,在不产生新记录的情况下修正最近一次的错误。
5.2.1. 提交修补:git commit --amend 的对象替换原理
场景一:修改提交信息
如果你只是想改一下刚才那条提交记录的备注:
1 | git commit --amend -m "fix: 修复了登录模块的空指针异常 (修正版)" |
场景二:追加漏提交的文件
如果你提交完才发现少加了 config.yaml:
1 | # 1. 先把漏掉的文件加入暂存区 |
底层原理:对象替换机制(危险!)
很多初学者认为 amend 是在“修改”旧的提交。这是一个巨大的误解。
在 Git 的对象存储模型中,Commit 对象是 不可变 的(Immutable)。一旦生成,它的内容、时间、父节点哈希就永久固定了。
当你执行 git commit --amend 时,Git 实际上做了以下三件事:
- 读取上一次提交(HEAD)的内容。
- 结合当前暂存区的内容(如果有),创建一个 全新的 Commit 对象(拥有全新的 SHA-1 哈希值)。
- 将 HEAD 指针从旧 Commit 移动到这个新 Commit 上。
旧的 Commit 对象并不会立即消失,它变成了一个 悬空对象(Dangling Commit),因为没有任何分支指向它,它最终会被 Git 的垃圾回收机制(GC)清理掉。
绝对红线:严禁在公共分支上执行 Amend
正因为 amend 会改变 Commit ID(哈希值),它本质上是在重写历史。如果你的代码已经 push 到了远程共享分支(如 develop 或 main),绝对不要在本地执行 amend。否则,当你再次 push 时,会因为历史线分叉而被拒绝(需要 force push),这会覆盖队友的代码,引发严重的团队协作灾难。
原则:Amend 仅限于还未推送到远程的本地私有提交。
5.2.2. 作者修正:使用 --author 临时覆盖提交身份
场景:你在家办公,使用个人电脑开发公司的开源项目。你的全局 Git 配置(Global Config)是个人邮箱 me@gmail.com,但公司要求提交必须使用 me@company.com。
你不想每次都改全局配置,也不想改项目的 .git/config(因为偶尔也要用个人身份提 PR)。
解决方案:单次覆盖。
1 | git commit -m "feat: corporate feature" --author="Alice <alice@company.com>" |
如果已经提交了,发现作者错了,可以结合 amend 进行修正:
1 | git commit --amend --author="Alice <alice@company.com>" --no-edit |
配置优先级回顾:--author 参数 > 项目级 .git/config > 用户级 ~/.gitconfig > 系统级 /etc/gitconfig。
5.2.3. 空提交应用:git commit --allow-empty 的工程价值
默认情况下,如果暂存区为空,Git 会阻止你执行 commit,提示 nothing to commit。但在 DevOps 工程化实践中,我们经常需要“无中生有”地创建一个提交。
命令:
1 | git commit --allow-empty -m "chore: trigger ci build" |
两大核心应用场景:
- 触发 CI/CD 流水线:有时候代码没变,但你需要重新运行流水线(比如之前的构建因为网络原因失败了,或者为了刷新缓存)。与其随便改个文件加个空格(Dirty Hack),不如提交一个优雅的 Empty Commit。GitLab/GitHub Actions 依然会识别到新的 Commit SHA 并触发构建。
- 初始化仓库的 Root Commit:在初始化一个新仓库时,很多架构师习惯先打一个空的
chore: initial commit。这样做的好处是,后续如果需要对第一次真正的代码提交进行 Rebase(变基)操作,会非常方便。因为 Git 的 Rebase 很难处理根节点(Root Commit),有一个空的根节点作为基座,能避免很多麻烦。
5.3. 提交信息的工程化规范
写好代码是程序员的本分,写好 Commit Message 是工程师的素养。一条模糊的 fix bug 或 update code 提交记录,是给未来维护者(包括两周后的你自己)挖的坑。
为了解决“提交信息随心所欲”的问题,Angular 团队提出了 Conventional Commits(约定式提交) 规范,这已成为目前全球最主流的工程化标准。
5.3.1. 提交信息的 “解剖学” 结构
一条标准的工程化 Commit Message 由 Header(标题)、Body(正文)、Footer(页脚) 三部分组成。
整体结构对比
| 组成部分 | 语法模板 | 真实案例 | 是否必填 |
|---|---|---|---|
| Header | <type>(<scope>): <subject> | feat(auth): add google login support | 必填 |
| Body | <body> | Integrate OAuth2 client with new security config. Note: requires env var GOOGLE_KEY. | 选填 |
| Footer | <footer> | Closes #128 | 选填 |
注意:各部分之间需要用 空行 分隔。
Header 结构分解
1 | fix(cart): prevent negative item quantity |
| Header 组成 | 说明 | 示例 | 要求 |
|---|---|---|---|
| Type | 提交的类别 | feat, fix, docs | 必填,使用约定的类型标识 |
| Scope | 影响的模块范围 | user, payment, cart | 选填,用括号包裹 |
| Subject | 简短描述做了什么 | add google login support | 必填,50 字符以内 |
Subject 书写规范
| 规范 | 正确示例 ✅ | 错误示例 ❌ | 原因 |
|---|---|---|---|
| 使用祈使句 | add user validation | added user validation | 应像给 Git 下命令 |
| 小写开头 | fix login bug | Fix login bug | 保持一致性 |
| 不加句号 | update readme | update readme. | 标题不需要句号 |
| 避免时态 | remove unused code | removes/removed code | 祈使句无时态 |
Body 写作对比
| 类型 | 示例 | 评价 |
|---|---|---|
| 废话文学 ❌ | 修改了 UserServiceImpl.java 的第 50 行逻辑。 | 看代码 Diff 就知道,无价值 |
| 有效说明 ✅ | 之前的 ID 生成算法在高并发下会产生主键冲突(复现率 1%)。本次改用雪花算法(Snowflake)彻底解决该问题,并兼容了旧数据格式。 | 解释了 Why 和 How |
Body 应该解释:
- Why:为什么要做这个改动(背景/问题)
- How:如何解决的(方案/思路)
- Impact:可能的影响(兼容性/性能)
Footer 常用标记
| Footer 类型 | 语法 | 效果 | 示例 |
|---|---|---|---|
| 关联 Issue | Closes #<issue-id> | 自动关闭对应 Issue | Closes #123 |
| 破坏性变更 | BREAKING CHANGE: <description> | 触发主版本号升级 | BREAKING CHANGE: The API v1/login is deprecated |
| 引用提交 | Refs: <commit-id> | 关联其他提交 | Refs: 68f1a3b |
| 审查者 | Reviewed-by: <name> | 记录代码审查者 | Reviewed-by: Alice <alice@example.com> |
这些 Footer 标记会被 GitHub/GitLab 等平台的自动化工具识别,用于触发相应的流程动作。
5.3.2. 语义化类型(Type)速查表
Type 是规范的核心,它决定了这次提交的性质。请对照下表,并在右侧查看 真实场景示例:
| 类型 | 含义 | 真实场景示例 (Syntax + Example) | 是否发布 |
|---|---|---|---|
| feat | 新功能 | feat(user): add email verification(用户模块:增加邮箱验证功能) | Minor |
| fix | 修补 Bug | fix(pay): handle timeout exception(支付模块:处理超时异常) | Patch |
| docs | 仅文档 | docs: update API swagger definition(更新 Swagger 接口文档) | 否 |
| style | 格式调整 | style: remove unused imports(删除未使用的引用,不改逻辑) | 否 |
| refactor | 代码重构 | refactor(order): simplify calculation logic(订单模块:简化计算逻辑,无功能变化) | 否 |
| perf | 性能优化 | perf: optimize image loading speed(优化图片加载速度) | Patch |
| test | 测试用例 | test: add unit test for login service(为登录服务增加单元测试) | 否 |
| chore | 杂项/构建 | chore: upgrade spring-boot to 3.2.0(升级依赖版本) | 否 |
| ci | 流水线 | ci: fix github actions script(修复 CI 脚本错误) | 否 |
5.3.3. 为什么要这么麻烦?(工程化价值)
这种细粒度的区分不仅仅是为了“整洁”,而是为了实现 自动化(Automation)。
如果你的团队严格遵守上述规范,配合自动化工具(如 standard-version),就能实现全自动流水线:
- 自动生成更新日志:工具扫描所有
feat和fix,自动生成精美的CHANGELOG.md。 - 自动计算版本号:
- 检测到
feat-> 自动将版本从1.0.0升至1.1.0。 - 检测到
fix-> 自动将版本从1.0.0升至1.0.1。 - 检测到
BREAKING CHANGE-> 自动将版本从1.0.0升至2.0.0。
- 检测到
这就是我们在序章中提到的“工程化思维”——用一次性的规范约束,换取永久的自动化便利。
5.4. 本章总结
本章我们完成了从“会提交”到“专业提交”的蜕变。我们不再将 Git 仅仅视为一个存档工具,而是将其视为一个精密的版本管理系统。
本章核心知识体系回顾
- 原子提交(Atomic Commit):
- 利用
git add -p将文件拆解为 Hunk,利用e模式拆解行级差异。 - 原则:一次提交只做一件事。这能极大降低回滚风险和 Code Review 的认知负担。
- 利用
- 历史修补(History Rewriting):
- 利用
commit --amend修正最近一次的失误。 - 铁律:只修补本地私有分支,绝不修补远程共享分支。
- 利用
- 工程规范(Engineering Standards):
- Commit Message 不是备忘录,而是给团队看的文档。
- 遵循
type(scope): subject格式,为未来的自动化运维(Auto Release)打下地基。
知识速查表
| 命令/概念 | 作用 | 关键参数/技巧 |
|---|---|---|
git add -p | 交互式暂存 | y(yes), n(no), s(split), e(edit) |
git restore --staged -p | 交互式移除暂存 | 反向操作,清洗暂存区 |
git commit --amend | 修补最近一次提交 | --no-edit (不改信息), --author (改作者) |
git commit --allow-empty | 允许空提交 | 用于触发 CI 流水线或初始化仓库 |
| Conventional Commits | 提交信息规范 | feat(新功能), fix(修复), docs(文档), chore(杂项), refactor(重构) |
常见问题
- Q: 我执行了 amend,但是现在的 Hash 值变了,之前的提交去哪了?
- A: 之前的提交变成了“悬空提交”(Dangling Commit),它依然存在于对象库中,但没有分支指向它。我们可以通过
git reflog找回它(下一章重点)。
- A: 之前的提交变成了“悬空提交”(Dangling Commit),它依然存在于对象库中,但没有分支指向它。我们可以通过
- Q:
git add -p太慢了,有图形化工具吗?- A: VS Code 的源代码管理面板、SourceTree 等工具都支持可视化选择 Hunk。但理解命令行
-p的原理,能让你在服务器端或无 GUI 环境下依然游刃有余。
- A: VS Code 的源代码管理面板、SourceTree 等工具都支持可视化选择 Hunk。但理解命令行
下一章预告
现在我们已经能够优雅地生成提交了,但如果在这个过程中我们犯了更严重的错误(比如误删了文件、提交了错误的版本并且已经 push 了),该怎么办?或者我们需要在不同的历史版本之间穿梭查看?
下一章 “第六章. Git 版本回退全攻略:详解 Reset、Revert 与 Reflog”,我们将掌握 Git 的“时光机”——Reset、Revert 和 Reflog,学会如何在任何绝境中找回代码。













