一、背景
- Git每次提交代码都需要写commit message,否则就不允许提交。一般来说,commit message应该清晰明了,说明本次提交的目的,具体做了什么操作......但是在日常开发中,大家的commit message千奇百怪,中英文混合使用、fix bug等各种笼统的message司空见怪,这就导致后续代码维护成本特别大,有时自己都不知道自己的fix修的是什么bug。
- 而近期,我们团队内有好几个大需求都需要多个研发同学all in一个开发分支,但是在开发过程中我发现很多研发同学提交的commit信息"五花八门",这就导致在定位问题时不知道对方或者自己的每笔提交是什么含义,无法快速定位问题。
- 基于以上这些问题,我们希望通过某种方式来约束用户的git commit message,让规范更好的服务于质量,提高大家的研发效率。一旦约束了commit message,意味着我们将慎重的进行每一次提交,不能再一股脑的把各种各样的改动都放在一个git commit里面,这样一来整个代码改动的历史也将更加清晰。
二、一般的commit message格式
2.1 文字版
注:如果不想看长篇文字的,可以直接看下面的流程图
对于一般的commit约束规范为:
- 检查提交者的邮箱 (user.email)
- 它会读取你本地 Git 配置的 user.email。
- 规则:
- 邮箱不能为空。
- 邮箱必须以 @xxx(公司名后缀).com 或 @xxx.net 结尾。
- 如果不满足,提交会被阻止,并提示你如何通过 git config 命令设置正确的邮箱。
- 检查提交者的姓名 (user.name)
- 它会读取你本地 Git 配置的 user.name。
- 规则:
- 姓名不能为空。
- 如果不满足,提交会被阻止,并建议你设置为中文名。
- 检查提交信息的编码
- 它会读取你写的提交信息文件。
- 规则:
- 提交信息必须是 UTF-8 编码。
- 如果不满足,提交会被阻止。
- 检查提交信息的格式 (核心功能)
- 这是这个hook最重要的检查。
- 规则: 提交信息的第一行必须以下列前缀之一开头:
- feat: (用于提交新功能)
- fix: (用于修复 bug)
- docs: (用于修改文档)
- style: (用于代码格式调整,不影响功能)
- refactor: (用于重构代码)
- test: (用于增加或修改测试)
- chore: (用于构建过程或辅助工具的变动)
- 例外情况: 它也允许以下 Git 自动生成的提交信息开头:
- Revert (当你执行 git revert 时)
- Merge branch (当你执行 git merge 时)
- cherry pick (当你执行 git cherry-pick 时)
- 如果不满足以上任何一种格式,提交会被阻止,并向你展示所有允许的格式和它们的含义,同时提供一个链接让你查看更详细的规范。
这里展开对具体格式提交格式讲讲
-
格式: :
- type(必填):用于说明git commit的类别,只允许使用下面的标识。
bash* feat:新功能(feature) * fix:修复bug * docs:文档更新 * style:格式更新(修改空白字符,格式缩进,补全缺失的分号等,没有改变代码逻辑) * refactor:重构代码(既没有新增功能,也没有修复 bug) * perf:性能, 体验优化 * test:新增测试用例或是更新现有测试 * revert:回滚到上一个版本。 * xxxff:代码合并。 * sync:同步主线或分支的Bug。 * build:主要目的是修改项目构建系统(例如 glup,webpack,rollup 的配置等)的提交
- subject(必填):subject是commit目的的简短描述。建议使用中文,结尾不加句号或其他标点符号。
-
示例:
- fix: 修改内存泄漏问题
- feat: 用户查询接口开发
2.2 流程图版
graph TD
A[用户执行 git commit] --> B{钩子开始执行};
B --> C{检查 user.email};
C -- 不合法 --> D[打印邮箱错误信息];
C -- 合法 --> E{检查 user.name};
D --> X([终止提交]);
E -- 不合法 --> F[打印姓名错误信息];
E -- 合法 --> G{检查提交信息的编码};
F --> X;
G -- 非 UTF-8 --> H[打印编码错误信息];
G -- UTF-8 --> I{检查提交信息格式};
H --> X;
I -- 格式正确 --> J([允许提交]);
I -- 格式错误 --> K[打印格式错误信息和规范];
K --> X;
subgraph "格式检查细节"
I1[获取 Commit Message] --> I2{"Message 是否以
feat: | fix: | docs: | style: |
refactor: | test: | chore:
Revert | Merge branch | cherry pick
开头?"} I2 -- 是 --> I; I2 -- 否 --> I; end style X fill:#f9f,stroke:#333,stroke-width:2px; style J fill:#9f9,stroke:#333,stroke-width:2px;
feat: | fix: | docs: | style: |
refactor: | test: | chore:
Revert | Merge branch | cherry pick
开头?"} I2 -- 是 --> I; I2 -- 否 --> I; end style X fill:#f9f,stroke:#333,stroke-width:2px; style J fill:#9f9,stroke:#333,stroke-width:2px;
三、不规范commit示例
在旧规范下依旧存在较多不规范的commit case,其中有些case是可以通过在提交前进行强制拦截的。
case | 问题 | 建议 |
---|---|---|
feat: update data for search | 全英文描述 | 尽量使用中文 eg:更新RN相关的资源 |
feat: 更新登陆样式&修改进入页面的链路 | 多处功能改动参杂着bug作为一笔提交 | 每个 commit 只完善一个功能/修复一个问题,保证提交commit的原子性 |
fix: ui样式修复 | 描述过于简单,无法根据commit知晓修改的功能 | 用简洁的语言描述具体的工作 eg: fix: 问一问落地页输入框样式修复 feat: DSL升级以支持Kitt引擎 |
feat: 提高效率 feat: 提高效率 | 多次重复提交同一条commit | 不要在同一个分支下提交重复的commit信息,如果解决的是一类问题,建议squash成一笔提交 |
feat: gjfahekfhsarjgjkajrgkntkzghndkztnbhknbhkgjdnthkjgnkdnhjk | 提交的字符过长 | 建议subject的字符数不超过72个。 (因为Git 工具通常在 72 字符处自动折行,超过会导致信息被截断,影响阅读。) |
fix: 修复了一个线上问题 | 描述信息subject的首尾存在多余的空格 | subject的首部至多有一个空格,尾部不应该含有空格 |
feat: 你好啊! | 末尾有特殊字符,比如?。等 | commit信息末尾不应该有特殊字符 |
四、制定新规范
4.1 强制约束类
- 格式:
<type>:[最多一个空格]<subject>[无特殊字符,无空格]
在原有规范的基础上,新增规范如下:
注⚠️:如果确实需要紧急合入,则可用
--force
命令,即git commit -m "feat: 这是一次紧急提交,忽略新增规范的约束 --force"
新规范对应的流程图如下:
图太大了,请将手机/电脑旋转90度"食用"
4.2 建议提倡类
- 原子性提交:每个 commit 只完善一个功能/修复一个问题
- 功能完整性:每个 commit 都应该是可编译、可运行的
- 逻辑分离:重构、优化和功能开发分开提交
- 语义明确:commit描述要清晰、简洁明了,尽量使用中文
五、hook脚本安装
- 创建hook脚本 已亲测无问题,对应的是第四章节的规范
bash
#!/usr/bin/ruby
#
# An example hook script to check the commit log message.
# Called by "git commit" with one argument, the name of the file
# that has the commit message. The hook should exit with non-zero
# status after issuing an appropriate message if it wants to stop the
# commit. The hook is allowed to edit the commit message file.
#
# To enable this hook, rename this file to "commit-msg".
# Uncomment the below to add a Signed-off-by line to the message.
# Doing this in a hook is a bad idea in general, but the prepare-commit-msg
# hook is more suited to it.
#
# SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p')
# grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1"
# This example catches duplicate Signed-off-by lines.
# test "" = "$(grep '^Signed-off-by: ' "$1" |
# sort | uniq -c | sed -e '/^[ ]*1[ ]/d')" || {
# echo >&2 Duplicate Signed-off-by lines.
# exit 1
# }
# "feat: 新功能提交"
# "fix: 修补bug"
# "docs: 文档修改"
# "style: 代码风格,不影响代码运行的变动"
# "refactor: 重构,即非新功能也非bug"
# "test: 测试用例修改"
# "chore: 构建过程或辅助工具的变动"
committer_email = `git config user.email`.force_encoding('UTF-8').strip
if (committer_email.nil?) or (committer_email.empty?)
puts "The email check failed, ensure that the Git \"user.email\" is not nil or not empty"
puts "The settings is \"git config --local user.email 'xxx@替换成公司后缀.com'.\" "
exit(1)
elsif !(committer_email.end_with? '@替换成公司后缀.com') && !(committer_email.end_with? '@替换成公司后缀.net')
puts "The email check failed, please use the email with the end of '@替换成公司后缀.[com|net]'"
puts "The settings is \"git config --local user.email 'xxx@替换成公司后缀.[com|net]'.\" "
exit(1)
end
committer_name = `git config user.name`.force_encoding('UTF-8').strip
if (committer_name.nil?) or (committer_name.empty?)
puts "The name check failed, ensure that the Git \"user.name\" is not nil or not empty"
puts "The settings is \"git config --global user.name 'chinese_name' (Suggestion).\" "
exit(1)
end
commit_msg = File.read(ARGV.first)
commit_msg.force_encoding('UTF-8')
if (commit_msg.valid_encoding? == false)
puts "Encoding is not utf-8"
exit(1)
end
if (commit_msg.start_with?("feat:") || commit_msg.start_with?("fix:") || commit_msg.start_with?("docs:") ||
commit_msg.start_with?("style:") || commit_msg.start_with?("refactor:") || commit_msg.start_with?("test:") ||
commit_msg.start_with?("chore:") || commit_msg.start_with?("Revert") || commit_msg.start_with?("Merge branch") || commit_msg.start_with?("cherry pick"))
else
puts "Commit message 格式错误"
puts "参考格式: https://xxx.md"
puts "feat: 新功能提交"
puts "fix: 修补bug"
puts "docs: 文档修改"
puts "style: 代码风格,不影响代码运行的变动"
puts "refactor: 重构,即非新功能也非bug"
puts "test: 测试用例修改"
puts "chore: 构建过程或辅助工具的变动"
exit(1)
end
####################### 新规范 start #######################
### 1. 自动修正首尾多余的空格,并给出警告
full_first_line = commit_msg.split("\n").first
original_full_first_line = full_first_line.dup
# 分割 type 和 subject
parts = full_first_line.split(':', 2)
type = parts[0]
subject = parts[1] || "" # 处理没有 subject 的情况
original_subject = subject.dup
# 修正 subject 的尾部空格
subject.gsub!(/\s+$/, '')
# 修正 subject 的首部空格(允许至多一个空格)
if subject.match?(/^\s{2,}/) # 检查是否存在超过1个的前导空格
subject.gsub!(/^\s+/, ' ') # 将多个前导空格替换为一个空格
end
# 如果 subject 发生了变化,则重组并更新
if subject != original_subject
new_first_line = "#{type}:#{subject}" # 直接拼接,不额外添加空格
puts "[警告⚠️] 检测到提交信息主题部分首尾有多余空格,已自动修正。"
# 重组整个 commit message
lines = commit_msg.split("\n")
lines[0] = new_first_line
commit_msg = lines.join("\n")
File.write(ARGV.first, commit_msg)
end
### 2. 基础规范检查完成,现在处理 --force 和新规范
# 检查提交信息内部是否包含 --force
is_force_commit = commit_msg.strip.end_with?('--force')
clean_commit_msg = is_force_commit ? commit_msg.gsub(/--force\s*$/, '').strip : commit_msg
# 如果是强制提交,将清理后的 message 写回文件
if is_force_commit
File.write(ARGV.first, clean_commit_msg)
end
# 获取最终的(经过空格修正后的)提交信息进行后续检查
final_commit_msg = File.read(ARGV.first).force_encoding('UTF-8')
final_first_line = final_commit_msg.split("\n").first.strip
# 检查 subject 是否为空(这个检查始终执行,即使是 --force)
final_parts = final_first_line.split(':', 2)
final_subject = (final_parts[1] || "").strip
if final_subject.empty?
if is_force_commit
puts "[警告⚠️] 提交信息的主题部分为空。【已强制提交message】"
else
puts "[错误❌] 提交信息的主题部分不能为空。"
puts "请提供有意义的提交描述,例如: feat: 添加用户语音搜索功能"
exit(1)
end
end
if is_force_commit
puts "[警告⚠️] 检测到 --force 标志,已跳过部分提交规范检查。"
# 检查第一行 commit信息的末尾不应该有。.??!!
if final_first_line.end_with?(".") || final_first_line.end_with?("?") || final_first_line.end_with?("!") || final_first_line.end_with?("。") || final_first_line.end_with?("?") || final_first_line.end_with?("!")
puts "[警告⚠️] 提交信息的第一行末尾不应包含标点符号 (. ? ! 。 ? !)。【已强制提交message】"
end
# 检查第一行 commit信息最大不超过72个字符
if final_first_line.length > 72
puts "[警告⚠️] 提交信息的第一行长度建议不超过 72 个字符 (当前: #{final_first_line.length})。【已强制提交message】"
end
# 在同一个git分支下,禁止提交信息与历史重复
current_branch = `git rev-parse --abbrev-ref HEAD`.strip
historic_commits = `git log #{current_branch} --pretty=format:%s`.split("\n")
if historic_commits.include?(final_first_line)
puts "[警告⚠️] 当前分支 \"#{current_branch}\" 已存在相同的提交信息。【已强制提交message】"
end
else
# 严格执行新规范检查
### 3. 检查第一行 commit信息的末尾不应该有。.??!!
if final_first_line.end_with?(".") || final_first_line.end_with?("?") || final_first_line.end_with?("!") || final_first_line.end_with?("。") || final_first_line.end_with?("?") || final_first_line.end_with?("!")
puts "[错误❌] 提交信息的第一行末尾不应包含标点符号 (. ? ! 。 ? !)"
exit(1)
end
### 4. 检查第一行 commit信息最大不超过72个字符
if final_first_line.length > 72
puts "[错误❌] 提交信息的第一行长度不能超过 72 个字符。"
puts "当前长度: #{final_first_line.length}"
exit(1)
end
### 5. 在同一个git分支下,禁止提交信息与历史重复
current_branch = `git rev-parse --abbrev-ref HEAD`.strip
historic_commits = `git log #{current_branch} --pretty=format:%s`.split("\n")
if historic_commits.include?(final_first_line)
puts "[错误❌] 当前分支 \"#{current_branch}\" 已存在相同的提交信息。"
puts "请修改提交信息以确保其独特性。"
exit(1)
end
end
####################### 新规范 end #######################
# echo "commit-msg: "
# echo $1
# exit 1
-
修改将 buildsystem/githooks 目录视为存放钩子脚本的地方
bashgit config core.hooksPath buildsystem/githooks
-
赋予执行权限
bashchmod +x buildsystem/githooks/commit-msg
-
将文件权限的变更添加到暂存区
bashgit add buildsystem/githooks/commit-msg
-
进行代码commit