ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

告别垃圾提交信息:一套可落地的Git提交规范实践

告别垃圾提交信息:一套可落地的Git提交规范实践 周末加班排查一个老项目的问题翻到一年前同事留下的提交记录整个仓库两千多次提交点开历史列表一眼望过去全是“fix”“update”“111”“asdf”这种信息。有两段提交甚至什么都没写就一个空字符串Git 都看不下去了给了个 “empty commit message” 的警告。我当时盯着屏幕上密密麻麻的垃圾信息心里只有一个念头如果当初有人好好定一套 Git 提交规范我现在至少能少翻半小时历史。这不是夸张。提交记录是代码仓库里唯一不会撒谎的“时间轴”但大多数人把它当成了草稿纸。很多人觉得提交信息随便写写就行反正代码能跑功能能上线。可等到你真要去查一个问题从哪个版本引入、某个功能为什么这样实现、某段逻辑是谁在什么背景下加的那些“fix”和“update”一点忙都帮不上。这篇文章我从一个最简单的现象说起把 Git 提交规范这件事拆开揉碎讲清楚包括为什么要做、具体怎么做、用什么工具卡住以及我在实际项目里踩过的坑。1. 为什么提交记录总是“无注释”——先搞清楚问题根源1.1 “能跑就行”心态是怎么养成的我见过太多开发者包括曾经的自己提交代码时只有一个朴素的想法赶紧把这版存下来别丢了。这种心态在单人开发或者项目初期特别常见反正就我一个人看我写什么别人也看不到写个“update”已经算给面子了更多人直接一个“。”或者干脆不写。但问题是你以为是“存个档”实际上你在给自己挖坑。两周之后你回来看那个“fix”根本想不起来修的是前端样式还是后端接口三个月之后你换了个项目再回来接手对着一条“update”只能靠时间戳和 diff 去猜。单人开发的“能跑就行”和多人协作的“能跑就行”是两回事前者坑自己后者坑整个团队。这里最隐蔽的问题是提交历史会被“小步提交”滥用。很多教程告诉你要频繁提交小步提交于是大家把提交当成了 CtrlS每改一行都提交一次信息全是“fix”或“working”。频繁提交本身没错错的是没有给每次提交赋予“独立语义”。提交的粒度可以小但每条提交记录都应该回答一个问题我这次改动到底完成了什么。1.2 没有统一“语言”提交记录就是噪音通道Git 本身不关心你写什么它只负责记录。但如果提交信息没有统一的格式和词汇表历史记录就成了一堆无法检索的碎片。“update”可以指任何事改了依赖版本、调了样式、删了文件、加了接口全部都可以叫“update”。老项目里有个人特别喜欢用 “Commit” 当提交信息三个提交全是 “Commit”打开 diff 一看一个是新增登录页一个是改数据库连接池一个是删除无用图片——你根本没法从信息层面判断这次提交做了什么。这不是文字洁癖是实实在在的成本。我在排查线上事故时经常要“回滚到某个提交之前”或者“查某个功能是哪个版本引入的”只要提交信息是垃圾定位就只能靠二分查找加肉眼比对效率极低。更离谱的是有些人提交时会把十几个文件的无关改动塞进同一条提交里信息只写一句“fix bugs”等于把一堆功能混在一起后续想单独回滚其中一个都做不到。我在一个项目里见过最夸张的例子有人把整个 release 分支合并回主干提交信息就一个“.”。你去看那次提交的 diff几百个文件几千行变更但信息只有一个句号。真要出问题你根本无从下手。1.3 真实代价生产事故复盘时没人说得清哪一次提交引入问题前两年我们有个服务线上报错率突然上升我习惯性地用 git bisect 去定位是哪个提交引入的回归。git bisect 是一个很强大的二分查找工具它能自动帮你从一段历史里找出第一次出现问题的提交。但这个工具的前提是提交历史得干净提交信息得可读。结果那次我把历史一拉从“fix”到“ok”到“test”到“.”根本不知道每个提交改了什么bisect 只能机械地告诉你某个 commit hash你还得自己一个个切出去跑测试、看 diff。更麻烦的是 code review。提交信息是 PR 评审的第一入口评审者看一个写着“update”的提交根本没法判断你这是有意为之还是夹带私货。没有规范的提交历史新人接手代码时连项目演进脉络都看不出来只能拿着源码硬啃。我算过一笔账一个三人的项目如果提交信息长期不规范新人上手时间至少多出 30% 到 50%而且这 30% 是纯纯的无效时间本来是可以避免的。所以 Git 提交规范这件事表面上是“给提交写个像样的说明”实际上是给团队的协作历史建立一个可索引、可回溯、可信任的信息通道。搞清楚这个前提你再看下面那些规则和工具就知道每一招背后都是在解决什么具体问题了。2. 可落地的提交规范长什么样——从约定式提交说起2.1 核心结构type(scope): subject 加 body 加 footer现在业界最通用的提交规范基本都遵循 Conventional Commits也就是约定式提交。它的核心结构很简单一条提交信息分三部分header必填格式是type(scope): subjectbody选填补充说明这次提交的背景和原因footer选填记录破坏性变更BREAKING CHANGE或关联的 issue 编号type 是动词表示这次提交的类型。我常用的词表固定有这么几类type含义典型场景feat新功能新增接口、新增页面、新增模块fix修复 bug修逻辑错误、修崩溃、修样式异常docs文档变更改 README、注释、接口文档style格式调整改缩进、加空格、修分号不影响逻辑refactor重构重写某段逻辑功能不变perf性能优化加快响应、减少内存占用test测试相关新增测试、修测试用例chore构建或杂项改配置、更新依赖、调整脚本ciCI 配置改流水线、改部署脚本revert回滚撤销某次提交为什么 type 一定要用固定词表而不是自由发挥因为只有词表固定提交历史才能被检索和统计。你写“fix”全仓库所有人搜“fix”就能筛出所有修复记录你写“改进”“优化”“解决”大家搜都不知道搜什么。固定词表是把提交记录变成结构化数据的第一步。举个例子一条规范的提交信息可以长这样fix(auth): 修复刷新令牌过期后 401 重定向循环 accessToken 过期后刷新接口返回 401前端没有做跳转处理 导致用户在无感知的情况下停留在空白页。这里在拦截器里增加 当 refreshToken 也过期时清除本地凭证并跳转登录页。 Closes #1234scope 是可选的作用域用来指明这次改动影响了哪个模块比如auth、api、ui。小项目可以不写但一旦模块边界清晰scope 能显著提升信息密度。我一般建议项目超过三个模块之后scope 就不要省略。2.2 为什么建议把“为什么”写进 bodyheader 只回答“做了什么”body 回答“为什么要这么做”。很多人习惯只写一行 header觉得 body 可省则省。但真实项目里代码 diff 只能告诉你“改了什么”永远不能告诉你“为什么改”而“为什么”恰恰是三个月后最值钱的信息。我见过一个反例提交信息写的是fix: 修改xx.java这条提交从字面上看毫无价值因为它只是重复了 diff 已经展示的“改了哪个文件”。正确的写法应该是说明原因“修改 xx.java 中的状态判断逻辑因为多租户场景下租户 id 可能为空原判断会导致空指针”。看到后面这句你才能理解这个改动的价值也才能在后续维护中判断这个修复是否可以被替代。写 body 有个实用技巧把提交当成给未来同事的一封短邮件。开头一行说清楚做什么下面两三句话说清楚为什么最多不要超过十行。如果你发现需要写很长的 body大概率你这次提交塞了太多东西应该拆开提交。我个人的经验是标准提交长度header 在 50 到 72 个字符以内body 每个自然段控制在 50 到 72 个字符以内这是 Git 原生推荐的宽度主要为了在终端和网页上展示时避免折行混乱。2.3 分支命名、PR 标题、merge 行为也要配套只规范 commit message 还不够分支命名的混乱同样会制造麻烦。分支名里最好带上类型和目的让人不看代码也能猜到这条分支在干嘛。我常用的是feature/功能名、fix/问题描述、docs/文档更新如果跟 issue 关联就写成fix/issue-1234-登录态丢失。这跟 commit message 一样本质上都是降低信息检索成本。PR 的标题我建议直接用这个分支上最重要那条 commit 的 subject不要另起一个风格。这样 PR 列表在 GitHub 或 GitLab 上扫过去一眼就能看出每个合并请求在做啥。merge 行为也要注意。Git 默认的git merge会把两个分支的提交历史交错在一起生成大量 “Merge branch develop into feature” 这类提交。这些提交本身没什么信息量只会让历史变乱。所以我在团队里会约定功能分支合并回主干时用git merge --squash或 PR 的 Squash and merge让这一整条功能分支的若干提交压缩成一个带规范信息的提交再合进主干。这样主干历史是一条干净的直线每条提交都有独立语义。对应的代价是你失去了功能分支内部的细节但对绝大多数项目来说主干历史的可读性远比分支内部细节重要。3. 用工具把规范“焊死”husky 加 commitlint 加提交模板3.1 本地提交前校验husky 加 commitlint规范写出来如果全靠自觉大概率坚持不了两周。所以必须用工具把规范“焊死”在流程里。最常用的组合是 husky 加 commitlint。husky 是 Git hooks 的管理工具它让你能在本地执行git commit时挂上自定义脚本。commitlint 专门负责校验提交信息是否符合约定式提交格式。安装非常简单在项目根目录执行pnpm add -D husky commitlint/cli commitlint/config-conventional然后初始化 huskypnpm exec husky init这条命令会在项目里生成一个.husky目录。我一般会在里面建一个commit-msg文件内容就是调用 commitlint 校验提交信息pnpm exec commitlint --edit $1再建一个commitlint.config.js内容可以这样module.exports { extends: [commitlint/config-conventional], rules: { type-enum: [2, always, [ feat, fix, docs, style, refactor, perf, test, chore, ci, revert ]], subject-case: [0], header-max-length: [2, always, 72] } };这里解释一下subject-case: [0]这条规则。默认配置会要求 subject 首字母大写但中文项目里很多人习惯用小写开头关掉这条规则可以减少无意义的报错。跑完之后的效果是提交信息不符合格式时git commit会被直接拦截命令行里会红字提示具体哪一条不符合。这一步就能把 80% 的垃圾提交挡在仓库之外。这里还要顺带说清楚 pre-commit 和 commit-msg 两个钩子的职责差异。很多人会混淆pre-commit 钩子应该在提交前跑代码检查比如 ESLint、Prettier、单元测试确保提交进去的代码是“干净”的而 commit-msg 钩子只负责校验提交信息本身两者各管一摊。我见过有的团队为了省事把代码检查和提交信息校验全塞进 commit-msg结果校验代码失败时提交信息都还没写报错信息非常混乱。正确分工是pre-commit 跑 lint-staged 管代码质量commit-msg 跑 commitlint 管信息质量。3.2 模板与 IDE 联动降低输入成本工具能阻挡格式错误但没法代替人思考。提交信息内容空洞的问题靠校验工具是解决不了的得靠模板来降低写好信息的门槛。我习惯在项目里放一个.gitmessage模板文件内容大概是这样的# type(scope): subject # 例如fix(auth): 修复登录过期后跳转循环 # 首行不要超过 72 个字符标题要能概括这次改动 # 空一行后写 body说明为什么这样改关联的 issue 编号然后执行全局配置git config --global commit.template ~/.gitmessage配置完之后当你执行git commit不带-m参数时Git 会打开编辑器并自动填入这个模板你只需要照着填。我实测下来这个模板让新人提交时几乎不需要思考格式只需要思考内容。还有人问我在 IDE 里怎么提交才算规范其实 IDEA 和 VS Code 的自带 Git 面板都支持git commit -m你可以写多行-mgit commit -m fix(auth): 修复刷新令牌过期后 401 重定向循环 -m accessToken 过期后刷新接口返回 401补充跳转登录页逻辑。第一个-m是 header第二个-m是 body两段会自动用空行分隔。如果你记不住这个技巧也可以直接敲git commit打开编辑器对着模板写。原则就一个让“照格式写”比“不照格式写”更省事规范才能持久。3.3 服务端兜底CI 里也跑一遍 commitlint本地 hook 不是万能的因为任何本地 hook 都可以用--no-verify参数跳过。我自己工作里见过不少人发现校验失败之后不是去改提交信息而是图省事直接加--no-verify硬提交。所以在团队项目里本地校验只是第一道防线还得在 CI/CD 流水线里加一道兜底校验。比如在 GitHub Actions 里可以加一个任务专门校验 PR 里的提交信息name: Check Commit Messages on: pull_request: jobs: commitlint: runs-on: ubuntu-latest steps: - uses: actions/checkoutv4 with: fetch-depth: 0 - uses: actions/setup-nodev4 with: node-version: 20 - run: npm install - run: npx commitlint --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }}GitLab CI 的写法也类似核心思路就是拉起一个最小环境装上 commitlint然后对 PR 涉及的所有提交做一次校验。这样就算有人在本地绕过 hook合并到主干前也会被流水线挡住。另外如果你的项目已经到了“提交规范已经稳定”的阶段可以进一步接入 standard-version 或 semantic-release让工具根据提交信息自动生成版本号和 CHANGELOG。这属于规范带来的额外回报我在 4.3 节再细说。4. 真实操作一览迁移老项目、处理常见坑4.1 老仓库“历史债务”怎么处理聊完怎么搭规范肯定会有人问我们已经有一堆乱历史了怎么办是不是要重写答案取决于这个仓库是否已经被团队共享。如果某个分支只在你本地还没推送到远程那你可以放心地用git rebase -i改写历史把一堆“fix”“update”压缩成一条规范的提交。操作步骤就是git rebase -i HEAD~10进入编辑器后Git 会列出最近 10 条提交你可以把其中的 “pick” 改成 “squash”这样多条提交会被合并成一条再补上一个规范的提交信息。也可以直接改成 “reword” 只改提交信息不改内容。但如果这个分支已经推送过而且有同事基于它开发那重写历史就会造成远程分支与本地不一致团队其他人 pull 的时候会撞上一堆冲突。这时候我不建议去动历史更务实的做法是“从今天开始执行新规范旧历史先放着”。你也可以在项目文档里写一份 CHANGELOG从今天起有人问“某个功能哪来的”你至少能回答“看这周以后的提交记录”。如果一个仓库已经乱到影响正常排查了非得改写历史一定要挑一个团队都同意的窗口期提前通知所有人统一 force push。这个操作不是不能做但绝对是高风险操作宁可慢也不要乱来。4.2 我在实际推行中踩过的坑我在团队里推行这套规范时踩过不少坑挑几个最典型的说说。第一个坑是“新人不会写”。规范文档写得再清楚到了实际操作时新人面对空白的编辑器还是会蒙。我的解决办法是先给模板再给示例最后让新人照着抄三条。先抄feat(api): 新增用户列表接口再抄fix(auth): 修复登录态失效问题抄完三条基本就明白格式了。这一招比我讲十分钟规则都管用。第二个坑是“提交粒度失衡”。有些人为了规范憋一整天就提交一次一次提交里塞了几十个文件信息写什么都显得空。有些人反过来每个文件都单独提交一次把历史切得稀碎。我的经验是一次提交应该是一个“可独立工作”的逻辑单元。比如“新增用户列表接口”可以包含后端接口、前端页面、测试用例这三者合起来是一个完整功能提交一次没问题但如果你同时在这次提交里改了数据库连接配置、修了一个日志 bug、加了一个页面弹窗那就该拆开了。判断标准就是如果这条提交要回滚你愿不愿意把它整体回滚掉。不愿意就说明它太杂了。第三个坑是“装饰性规范”。commitlint 校验通过了提交信息却依然是废话比如fix: fix。commitlint 只能拦格式拦不住空话。这个问题的答案还是回到 3.2 节的模板和 2.2 节的 body 要求上你得在团队约定里明确“subject 必须能脱离 diff 独立描述改动”否则就打回重写。虽然没有工具能自动判断内容是否有意义但 review PR 的时候顺手看一下提交信息这个成本很低效果却很好。第四个坑是“hook 装不上”。很多老项目用的是 npm 或 yarn有的机器 Node 版本过低husky 的 install 脚本会不兼容直接报错。这时候先看看 husky 版本是否与 Node 版本冲突或者手动执行一下npm run prepare来生成 hooks。还有一个常见问题是团队里有人的 IDE 自带的 Git 工具没有触发 husky 的钩子导致本地校验形同虚设。解决办法是统一约定涉及代码提交一律走命令行或者确认 IDE 的 Git 插件确实触发了 hooks别让 IDE 静默绕过。给一个常见问题的排查速查表现象可能原因解决办法提交时 commitlint 报错但不显示具体内容配置格式错误或插件没装全检查commitlint.config.js是否可被正确加载重新安装依赖husky 生成的 hook 不触发prepare 脚本没执行在项目里运行npm run prepare或重装 node_modules有人用--no-verify绕过本地成本高或意识不足靠 CI 兜底同时在团队约定中说明为何禁止绕过IDEA 提交时校验不生效IDE 的带权限执行被限制在设置中启用 “Allow running in background”或引导成员用命令行提交提交信息格式对但内容空洞缺少模板和示例引入.gitmessage模板明确 subject 必须能独立描述改动4.3 从提交规范到发布自动化changelog 的生成最后说一个最有“回报感”的部分。当你的提交信息稳固地遵循约定式提交之后你就可以让机器来帮你生成版本号和 CHANGELOG。我常用的工具是 standard-version安装后先配置 package.jsonnpm i -D standard-version然后在 package.json 里加一段脚本{ scripts: { release: standard-version } }跑npm run release的时候它会扫描从上一个 tag 到现在的所有提交根据提交里的feat、fix、BREAKING CHANGE自动决定版本号是升 minor、patch 还是 major并生成一份 CHANGELOG.md。比如这一轮只修了几个 bug版本就从 1.2.3 升到 1.2.4如果有一个不兼容变更它就会升到 2.0.0 并标注 BREAKING CHANGE。这一套流程跑通之后你会发现规范提交这件事不再是你“多花时间给别人看”的义务而是你“少花时间自己干活”的杠杆。你不需要再手工维护发版记录不需要在发版前翻历史整理变更机器直接从规范的提交里自动提取信息。我见过一些团队连 git tag 都懒得打版本全靠 package.json 里的数字猜结果线上跑了什么功能都说不清。接入 standard-version 后版本历史和提交历史统一了这带来的效率提升是非常直观的。最后分享一个我自己的体会这套东西我在团队里推行了两年多最大的感受是提交规范真正难的永远不是那几条规则而是坚持和习惯。规则可以抄工具可以装但如果你只是“为了规范而规范”把提交信息写成了“看起来专业”的形式却没有真正去思考每次改动的原因那最后还是会被历史惩罚。我个人的建议是从今天开始哪怕只有你一个人在维护项目也先用固定的 type 提交哪怕是单人的 GitHub 仓库也装一遍 commitlint。攒到一百条有规律的提交记录以后你再回头看看会发现那个曾经全是“fix”和“update”的历史变成了你可以快速检索和依赖的真实项目日志。再送你一个小技巧把 type 词表做成一条 markdown 贴在终端窗口旁边或者存成 IDE 的片段代码写的时候瞄一眼就好。等你哪一天看到一条老提交不用打开 diff 就能准确说出它改了啥你就明白这件事的价值了。
返回列表