假设你住在一栋 12 人合租的大房子里,玄关有一块公共大白板。谁换了客厅的灯泡、修好了浴室的漏水水龙头,都要在白板上贴一张便签做记录。一开始大家很随意:有人写“今天弄了点东西”,有人写“update“,还有人画了三个句号”。。。“。三个月后,新搬来的室友想搞明白:浴室那个水龙头是什么时候、被谁、怎么修好的?他站在白板前看了十分钟,得到的全部信息只有一句——”反正有人修过”。这就是没有 Commit Message(提交信息)规范时的日常。 本文要介绍的 Conventional Commits(约定式提交),就是一套给开发社区定制的“便签公约”。它规定:每张便签必须写清“变更类型、影响范围、一句话描述”;遇到“拆墙级别”的重大改动,还必须贴上醒目的警示标志。读完你会发现,一行小小的提交信息,其实是撑起自动 Changelog、语义化版本和高效 Code Review 的整个体系的基石。
对 AI 从业者来说,规范的 Commit Message 是 PR 被快速合并的“隐形敲门砖”:它能帮 reviewer 快速信任你的每次变更,也能让 AI 编程助手更好地理解你的项目历史。
可视化演示
展示Conventional Commits结构化提交、Breaking Change标注与CI自动生成Changelog的过程,可切换三种模式观察提交规范落地。
本节配有交互动效(进入视口后自动加载,约 3KB)。
Conventional Commits格式 结构化type前缀对提交分类,如feat、fix、docs 格式为type(scope): description,type必填 feat(api): add endpoint 结构化提交提升可追溯性。
提交信息意义
合租屋里的便签,如果不是写给三个月后的自己看的,那它写来做什么?真正需要追溯历史时,白板上只有“update“和”。。。“——这就是无规范提交的日常。你也许记得昨天改了什么,但一个月后呢?三个月后呢?Git 把每一次修改都永久记在了”白板”上,可如果每张便签都语焉不详,这个白板就只是一堆乱码。
放到技术语境里,Commit Message(提交信息)是一种异步沟通:你写下它的时间点,和有人阅读它的时间点,往往相隔数周甚至数年。阅读者可能是未来的你、接手代码的同事、审阅 PR 的维护者,也可能是自动生成 Changelog 的机器人。一篇好的提交信息,应当回答五个问题:改了什么、为什么改、影响哪里、谁改的、什么时候改的。其中“谁”和“什么时候”由 Git 自动记录,真正需要你亲手写的,是前三个。
一份有意义的提交记录,本质上由三块信息构成:
从此,合租屋的白板不再是“鬼故事合集”。每个人写便签时多花三十秒,未来所有人省下三小时。这正是 Commit Message 规范存在的第一层理由:让历史真正可读、可追溯。
- 类型(Type)先用一个动词标签说明“这是哪种变更”,让机器和人都能快速分类。
- 主题(Subject)用一句话说清“做了什么”,控制在约 50 字符内,像便签标题一样醒目。
- 理由(Body)在正文里补充“为什么这么做”,包括背景、动机与注意事项。
约定式提交格式
“便签公约”说起来很简单,难的是所有人写法一致。有人写“修了一下”,有人写“fix bug“,有人写”fix:bug”(冒号后居然没有空格)。于是社区在 2018 年前后定下了一份公开的公约——Conventional Commits(约定式提交)。它把便签模板固定成一行,谁都能照着写,机器也能照着读。
约定式提交把一条提交信息分成三部分:头部(header)、正文(body)、页脚(footer)。其中 header 是必填的,也是最核心的:
写完 header 后,如果这次变更需要交代背景,就在空一行后写正文(body);如果涉及破坏性变更或需要引用 Issue,就在末尾写页脚(footer)。这样一张“三层便签”就完整了:黄纸条写摘要,白纸区写原因,蓝角贴引用。合租屋从此告别“便签百家争鸣”的时代,扫一眼白板就知道每张便签在说什么。
- type(类型)必填,小写英文单词,例如
feat、fix。 - (scope)(范围)可选,用英文括号包裹影响的功能模块。
- !(破坏标记)可选,写在右括号后或 type 后,表示不兼容变更。
- 冒号与空格半角冒号后必须跟一个空格,这是机器解析的分界符。
- subject(描述)一句话说清变更内容,一般小写开头,句末不加句号。
类型分类体系
既然便签要分类,那标签总共分几种?约定式提交最常用的是 feat 和 fix,其他还有 docs、refactor、perf、test、chore、style、build、ci 等。你可以把它们理解为合租屋里的不同颜色贴纸:绿色代表“添了新东西”,红色代表“修好了坏东西”,蓝色代表“更新了说明书”。
选择类型时有一个核心判断标准:这次变更对“使用代码的人”来说,外部行为变了吗? 是新增了能力,还是纠正了错误,还是纯粹内部整理?这个判据可以用一个分段函数表达:
这就像管家按颜色整理周报:新增的家具列表归一类,修好的水龙头归一类,重摆的沙发归一类。类型选对了,后续的自动归类、版本推断、历史检索才全部成立。
- feat 的判定用户获得了前所未有的能力,比如新增登录接口。
- fix 的判定行为与预期不符,被修正回正确状态。
- refactor 的判定重写内部结构,但外部调用方式完全不变。
- 分类互斥一次提交只选一个最贴切的类型,不要写成“feat 同时修复了 bug”这种四不像便签。
Scope范围标注
便签上只写“fix: 修好了”,室友还是会追问:“你修好了哪里?浴室还是厨房?” 这就是 Scope(范围)要解决的问题。在约定式提交中,scope 写在 type 后面的括号里,用来标注这次变更影响的模块或区域。
例如 fix(auth): 登录成功后跳转回原页面,括号里的 auth 就是认证模块。有了它,谁动了“浴室”一目了然,住在隔壁的室友(维护其他模块的同事)立刻警觉:这次改动会不会波及我?
scope 不是必填项。如果一次变更横跨多个模块,写不写某个具体模块都不准确,这时可以省略括号——含义是“影响范围较大”。但团队一旦约定用 scope 区分模块,就能用 git log --grep="fix(auth)" 这样的命令快速过滤某个模块的全部修复。这就像给每张便签贴了“浴室”“客厅”的标签,将来检索某个空间的历史时,一抽目录就全出来了。
- fix(类型)这里是缺陷修复,所以用
fix。 - 括号(范围容器)英文圆括号,紧跟在 type 后面,中间无空格。
- auth(范围名)用简短的小写单词或短横线表示模块,如
auth、ui、api。 - 冒号空格右括号后紧跟半角冒号和一个空格,再接描述。
- subject(描述)一句话说清这次修复的具体效果。
破坏性变更
合租屋不是没出现过“大事”:有人把客厅的墙拆了改成了健身房。这种事如果只贴一张绿色便签“feat(客厅): 新增健身器材”,第二天全屋人都会懵——昨天还能在客厅跑步,今天跑步机都放不下。这类“旧用法被推翻”的变更,就是 Breaking Change(破坏性变更)。
约定式提交为破坏性变更设计了两个动作。第一,在 header 的类型或 scope 后面加一个感叹号 !,相当于在便签正面盖了一个“危险”红章;第二,在页脚(footer)里必须以 BREAKING CHANGE: 开头,详细说明“旧用法如何迁移到新用法”。
为何要如此大费周章?因为破坏性变更直接影响版本号——它对应语义化版本中的 MAJOR(主版本号) 升级。一个 ! 加上一段迁移说明,既能让使用者第一时间评估风险,又能让发布工具自动把版本从 1.4.2 跳到 2.0.0。拆墙之前先广播,全屋人才不至于在健身房门口撞个头破血流。
- feat(类型)表面看是新增能力,实际是移除旧接口,因此仍可选
feat。 - (api)(范围)影响的是 API 模块。
- !(破坏标记)右括号后紧跟感叹号,表示“这不是普通更新,是拆墙级变更”。
- 冒号空格与 subject标记之后正常写冒号空格和描述。
- footer 迁移说明正文后空行,写
BREAKING CHANGE:加迁移指南,告诉使用者如何从 v1 升级到 v2。
PR流程落地
在开源项目中,便签不是贴完就完事的,它要先经过“议事会”审阅——这就是 Pull Request(PR,拉取请求)。当你在 GitHub 上提交 PR 时,PR 的标题、描述、以及里面的每个 commit message,共同组成一份“变更提案”。提案写得越规范,维护者审阅的速度就越快,被合并的概率也越高。
PR 流程中践行 Commit Message 规范,有四个关键动作。第一,PR 标题遵循同一套格式,比如 feat(ui): 新增深色模式,让维护者在 PR 列表中一眼识别类型;第二,在描述中关联 Issue,写 Closes #123,让需求、讨论和代码闭环;第三,把提交拆分成原子化的小步,每一张便签只做一件事;第四,如果采用 Squash 合并(把多个提交压缩成一条),最终那条提交信息也必须是规范的。
在议事会上,reviewer 看到一个标题清晰、描述完整、提交历史条理分明的 PR,就像拿到一份装订整齐的施工方案,而不是一摞乱纸。规范的 Commit Message 不只是写给自己看,更是整个 PR 审阅流程的“沟通基础设施”。
- 规范标题(Title)PR 标题沿用
type(scope): subject格式,作为整个 PR 的“封面便签”。 - 背景描述(Description)用自然段说明问题背景与测试结果,帮助 reviewer 快速进入上下文。
- 关联 Issue使用
Closes #123让 PR 合并时自动关闭对应 Issue。 - 原子化提交(Commits)把大改动拆成多个单一意图的 commit,每个 commit 本身也是一条规范便签。
自动化工具链
规范还有一个隐藏红利:机器可读。当每张便签都遵循 type(scope): subject 的统一格式时,管家机器人就能自动帮你干活了。合租屋的公告栏编辑(Changelog 生成工具)会每周自动把便签整理成周报:新增了什么、修复了什么、有什么重大变更——不需要任何人手工汇总。
整个自动化链条的枢纽是语义化版本(Semantic Versioning),形式为 主版本号.次版本号.修订号(MAJOR.MINOR.PATCH)。约定式提交和它之间有一条黄金映射规则:
现实中,commitlint 会在 CI(持续集成)里检查提交信息格式,格式不对直接让流水线变红;semantic-release 会在合并到主干后自动发版;Changelog 则按"Added / Fixed / Breaking Changes"分门别类生成。合租屋从此有一个不知疲倦的管家,每周自动贴出公告。人类要做的只剩一件事:好好写好每一张便签。
- fix → PATCH缺陷修复发布后,只递增修订号,如
1.2.4 → 1.2.5。 - feat → MINOR向后兼容的新功能,递增次版本号并清空修订号。
- breaking → MAJOR不兼容变更,递增主版本号。
- 工具执行
semantic-release这类工具读取每次提交的类型,自动计算新版本号、生成 Changelog 并发布。
AI 应用场景
MLOps 训练仓库的版本发布
阶段:模型实验与发布管理
解决的问题:AI 项目里训练代码、数据处理逻辑、模型配置频繁变动,提交信息一乱,就说不清“模型指标掉两个点”到底是哪次改动引起的。规范提交后,每次实验的关键变更都有据可查。
具体案例:某 MLOps 团队用约定式提交管理训练仓库,提交像 perf(trainer): 将 batch size 调至 128、feat(data): 新增数据增强 pipeline。发布新 checkpoint 时,脚本自动扫描提交历史,把 feat 和 fix 归类生成简洁的 Changelog,直接发给合作算法团队。
PR 审阅时的机器人把关
阶段:代码审阅与 CI 自动化
解决的问题:AI 工程师的 PR 里常混有实验性代码、格式化改动和临时调试输出,reviewer 很难分辨重点。机器人在合并前拦截不合规的提交信息,让审阅回归代码本身。
具体案例:团队在 GitHub Actions 中配置 commitlint,PR 标题不符合 type(scope): subject 格式时自动标记为失败,并要求作者修改后再审。PR 列表按 feat / fix 过滤,维护者能优先处理紧急修复。
线上事故的快速回溯
阶段:线上问题排查与回溯
解决的问题:模型服务上线后效果下滑,传统做法是逐个看提交 diff,大海捞针。有了规范提交,可以按类型和模块精确检索历史。
具体案例:服务指标异常后,工程师执行 git log --grep="feat(data)" --oneline,秒级定位到最近一次数据模块的功能变更,结合回滚操作快速恢复服务。
Python 演示
import refrom collections import defaultdict PATTERN = re.compile( r"^(?P<type>feat|fix|docs|refactor|perf|test|chore|build|ci|style)" r"(?:\((?P<scope>[^()]+)\))?(?P<breaking>!)?: (?P<subject>.+)$") COMMITS = [ "feat(auth): 新增短信验证码登录", "fix(cart): 修复结算金额精度丢失", "feat(api)!: 移除v1查询接口", "docs(readme): 更新快速开始文档", "refactor(utils): 抽取日期格式化函数", "update 乱七八糟的提交",] SECTION_MAP = { "breaking": "Breaking Changes", "feat": "Added", "fix": "Fixed", "docs": "Docs", "refactor": "Refactor", "perf": "Performance",} def parse_commit(message): m = PATTERN.match(message.strip()) if m is None: return None return m.groupdict() def build_changelog(commits): groups = defaultdict(list) for msg in commits: info = parse_commit(msg) if info is None: print("ignored:", msg) continue key = "breaking" if info["breaking"] else info["type"] scope = "(" + info["scope"] + ") " if info["scope"] else "" groups[key].append("- " + scope + info["subject"]) lines = ["# Changelog", ""] for key in ("breaking", "feat", "fix", "docs", "refactor", "perf"): if key not in groups: continue lines.append("## " + SECTION_MAP[key]) lines.extend(groups[key]) lines.append("") return "\n".join(lines) if __name__ == "__main__": print(build_changelog(COMMITS))
r"^(?P<type>feat|fix|docs|refactor|perf|test|chore|build|ci|style)"用正则锚定字符串开头,通过命名分组抓取类型r"(?:\((?P<scope>[^()]+)\))?"匹配括号包裹的 scope,整个分组可省略r"(?P<breaking>!)?"匹配可选的感叹号,用于识别破坏性变更r": (?P<subject>.+)$"冒号加一个空格后,捕获剩余内容作为描述if info is None:解析失败说明提交不合规,打印ignored后跳过key = "breaking" if info["breaking"] else info["type"]优先把破坏性变更单独归为一类groups[key].append("- " + scope + info["subject"])按类别把内容追加到对应分桶if __name__ == "__main__":直接运行时输出排版好的 Changelog 文本
常见误区
误区我觉得提交信息就要把改动的来龙去脉全写进去,所以经常一口气写三四百字,恨不得把每个变量的变化都交代清楚。
正解提交信息不是论文,而是有结构的便签。header 保持一句话摘要(约 50 字符),背景放在 body,Issue 引用和破坏性说明放 footer。就像合租屋便签:正面写“修好浴室水龙头”,背面才写“用了什么型号的垫圈”。信息详细不等于信息有序,无序的长文反而没人读。
为何常见:新手怕“说不够”导致未来看不懂,于是用堆字数来缓解焦虑,却忽略了结构化表达才是可读性的关键。
误区看到开源项目都是英文 commit,觉得写中文不专业,于是中英混杂地写“fix 了 bug,优化 login 逻辑”。
正解约定式提交规范只约束格式和类型词(如 feat、fix),不约束描述语言。公司内部仓库完全可以用中文写 subject,关键是格式一致、表意清晰。就像合租屋的便签,大家约定好格式模板,内容用全屋人都懂的语言即可。
为何常见:把“开源惯例”误解为“必须用英文”,忽略了规范的本质是沟通效率而非语言形式。
误区我学会了 `feat(xxx): 描述` 的模板后,每次提交都套格式,但经常一条提交里塞了三四个改动,类型也随便选。
正解规范的核心是“一次提交只做一件事”,格式只是它的外壳。如果一条提交既改了 bug 又加功能还动了文档,哪怕格式满分,未来的 git bisect(二分定位 bug)和代码回滚都会痛苦不堪。就像一张便签既写“修水龙头”又写“买新沙发”,执行时根本没法单独撤销其中一件事。
为何常见:人天生倾向于“攒一批改动一起提交”以减少操作次数,却忽略了变更的可独立回溯性。
误区破坏性变更不就是在 header 里加个感叹号吗?加完就大功告成,页脚不用写。
正解! 是“警示灯”,BREAKING CHANGE: 页脚才是“使用说明书”。没有迁移说明,使用者只知道“坏了”,不知道“怎么改”。项目维护者看到不带迁移指南的 breaking commit,通常直接打回。就像你在门口贴“健身房改造,勿入”,却不告诉邻居去哪里跑步,这不算负责任的通知。
为何常见:写 ! 只需一秒,写迁移说明需要思考,人性倾向于用最省力的方式完成“看似合规”的提交。
误区代码写得好才是本事,commit message 只是给 git 看的备注,反正功能正常就行。
正解一旦项目经历数月迭代,提交历史就是团队唯一的“变更记忆”。Changelog 由它生成,版本号由它推断,线上事故靠它回溯。代码写得好是“做对了事”,提交信息写得好是“说清了事”。合租屋的白板若是天书,房子再豪华,住客也会迷路。
为何常见:人在编码时注意力集中在功能实现上,容易把“收尾记录”视为低价值工作,低估了长期协作中的信息成本。
面试题
Q1. Conventional Commits 中 header 的标准格式是?
- A.
<type>(<scope>): <subject>← 正确答案 - B.
<scope>(<type>): <subject> - C.
<type>: <scope>(<subject>) - D.
<subject>(<scope>): <type>
答案:A。header 的顺序是 type 开头,可选 scope 用括号跟在 type 后,冒号空格后接 subject,即 type(scope): subject。其他选项要么类型和范围顺序颠倒,要么冒号位置错误,都不符合约定式提交规范。
Q2. 关于 feat 和 fix 的区别,正确的是?
- A.feat 用于缺陷修复,fix 用于新增功能
- B.fix 用于缺陷修复,feat 用于新增功能← 正确答案
- C.两者可以随意混用
- D.只有 fix 是标准规范类型
答案:B。feat 对应新增对使用者可见的功能,fix 对应修复与预期不符的缺陷。就像便签分类:绿色贴纸代表“添了新家具”,红色贴纸代表“修好了坏东西”,二者语义不同,不可混用。
Q3. 关于 scope(范围),下列说法错误的是?
- A.可以描述影响的功能模块
- B.每次提交都必须填写← 正确答案
- C.写在 type 后面的括号里
- D.省略时表示影响范围较大
答案:B。scope 是可选字段,不是必填项。如果变更横跨多个模块,写某个具体模块反而不准确,此时可以省略括号。B 说“必须填写”是错误的,其余三项均符合规范。
Q4. Breaking Change 的感叹号 ! 应该加在哪里?
- A.在 type 或 scope 后面加!← 正确答案
- B.在 subject 末尾加上!
- C.在正文首行添加!
- D.在提交说明中不需要
答案:A。! 加在 header 中 type 或 scope 的右括号之后、冒号之前,例如 feat(api)!: 移除v1接口。加在 subject 末尾或正文首行都不符合规范,D 更是忽略了破坏性变更必须醒目标注的要求。
Q5. 页脚(footer)中标注破坏性变更的固定关键词是?
- A.BREAKING CHANGE:← 正确答案
- B.BREAK CHANGE:
- C.CHANGE BREAKING:
- D.BREAKING:
答案:A。约定式提交规定 footer 中的固定短语必须是 BREAKING CHANGE:,后面跟迁移说明。它相当于拆墙公告上的“使用说明书”。其他词序写错或省略 CHANGE 都不被工具识别。
Q6. 以下哪些属于约定式提交的合法类型?
- A.feat 和 fix← 正确答案
- B.docs 和 style← 正确答案
- C.good 和 nice
- D.refactor← 正确答案
答案:ABD。标准类型包括 feat、fix、docs、style、refactor、perf、test、chore 等。C 选项“good“和”nice”不是规范中的类型词,工具不会识别。
Q7. 一条完整的约定式提交信息可以由哪些部分组成?
- A.header 头部← 正确答案
- B.body 正文← 正确答案
- C.footer 页脚← 正确答案
- D.必须包含二进制签名
答案:ABC。header 必填,body 和 footer 按需填写。header 是摘要,body 写背景,footer 放破坏性说明或 Issue 引用。D 的二进制签名不是约定式提交的组成部分。
Q8. 提交信息 feat(api)!: 移除 v1 查询接口 触发的新版本号变化是?
- A.递增 PATCH 版本号
- B.递增 MINOR 版本号
- C.递增 MAJOR 版本号← 正确答案
- D.保持版本号不变
答案:C。! 表示 Breaking Change,对应语义化版本中的 MAJOR(主版本号)升级,例如从 1.3.0 升到 2.0.0。如果只有 fix 则升 PATCH,只有 feat 则升 MINOR。
Q9. 规范 Commit Message 能带来哪些直接价值?
- A.自动生成 Changelog← 正确答案
- B.按类型筛选提交历史← 正确答案
- C.驱动语义化版本升级← 正确答案
- D.强制统一代码风格
答案:ABC。机器读取类型后能自动归类生成 Changelog、按 --grep 过滤提交、推断版本号递增。但提交信息规范与代码风格(如缩进、命名)无关,D 不属于它的直接价值。
Q10. 在 PR 采用 Squash 合并时,推荐的最终提交信息写法是?
- A.直接沿用第一条提交信息
- B.使用规范的汇总提交信息← 正确答案
- C.拼接所有中间提交信息
- D.只保留 PR 编号,不写描述
答案:B。Squash 合并会把多个提交压缩成一条,最终提交信息应是一条重新组织的规范信息,如 feat(ui): 新增深色模式,并通常引用 PR 编号。直接沿用第一条或机械拼接都会丢失整体上下文,不写描述则让历史无法追溯。