Skip to content

第七章 Git 提交规范

7.0 前言

Git 是目前程序员必备基础技能,可以用来管理代码、文档、博客,甚至菜谱。个人的私有仓库的提交相对而言可以

较为随意,但是在团队开发中,还是要遵循相应的规范。本文对 Git 使用中涉及到提交相关的实践做了些整理,

供大家参考。

git commit

如上图所示(截取自 Angular commit 970a3b5),

一个 commit 包含如下几个信息:

  • commit message - 提交的内容相关描述
  • author & committer - 作者及提交者
  • changed files - 修改的文件
  • hash & parent - 提交内容的 hash 及在提交树上的位置

7.1 Commit Message

提交消息描述的是当前提交的功能相关信息,一般可以包括 headerbodyfooter

bash
<header>
<BLANK LINE>
<body>
<BLANK LINE>
<footer>

业内做的好的可以参考 Angular 的提交标准:Commit Message Format

其中 header 是必须的。Angular 官方建议的格式如下

bash
<type>(<scope>): <short summary>

             └─⫸ Summary in present tense. Not capitalized. No period at the end.

       └─⫸ Commit Scope: animations|bazel|benchpress|common|compiler|compiler-cli|core...

  └─⫸ Commit Type: build|ci|docs|feat|fix|perf|refactor|test

<header> 中,<type><summary> 是必须的,<scope> 可以选填。建议 <header> 需要保持在 50 个字符之内。

<type> 表明本次提交的类型,一般有如下几种:

  • build: 涉及构建相关的改动
  • ci: 持续集成相关的改动
  • docs: 文档
  • feat: 新功能
  • fix: bug 修复
  • perf: 性能相关改动
  • refactor: 重构相关(非 bug、非新功能)
  • test: 测试相关,包括新增测试或者更改已有测试

<scope> 表示改动影响的范围。在 Angular 中,某个提交可能涉及的范围有表单处理、动画处理等。

在实际工作中可以视项目而定。

<summary> 则是对本次提交的简要描述,使用祈使句、现在时。如使用 change 而不是 changed

changes

<body> 是提交信息的更为详细的描述,与 <header> 一样也是用祈使句、现在时。<body> 描述本次

修改的动机,比如为什么引入本次改动,之前的逻辑是什么,现在的逻辑是什么,本次改动有哪些影响,等等。

最后,<footer> 是可选项,一般涉及破坏性改动、功能的弃用等说明,以及对 GitHub issue

Jira ticket 的引用,PR 的引用等。

规范的提交信息可以使用工具对内容进行解析,自动化生成文档或者发布日志。在一些大型的开源项目中,

版本的更新文档,接口的更新及兼容性影响,纯粹靠人工整理是很费时费力的,用统一的规范能够极大自动化

这部分工作。当然不同的项目对提交信息的要求和格式标准也不一样,开源项目或者公司项目对提交信息的

要求也有差异,一般需要遵从所在项目的约定。较为成熟的开源项目一般可以在 README 文档中找到如何贡献,

或者有单独的 CONTRIBUTING.md 文档,对代码风格、提交方式等进行约定。

7.1.1 自动化校验 commit message

有了提交信息的规范,如何确保开发者对规范进行遵守呢?我们可以使用 Git 提供的 Git Hooks 功能对提交

的信息进行校验。本文不对 Git Hooks 的细节做过多介绍,仅做基础的说明,具体细节可以参考

官方文档

Atlassian的文档

在新初始化的 git 项目内,我们可以在 .git/hooks 文件夹中找到官方提供的样例:

shell
ls -l .git/hooks
total 120
-rwxr-xr-x  1 tomo  staff   478B Nov 11 20:44 applypatch-msg.sample
-rwxr-xr-x  1 tomo  staff   896B Nov 11 20:44 commit-msg.sample
-rwxr-xr-x  1 tomo  staff   4.5K Nov 11 20:44 fsmonitor-watchman.sample
-rwxr-xr-x  1 tomo  staff   189B Nov 11 20:44 post-update.sample
-rwxr-xr-x  1 tomo  staff   424B Nov 11 20:44 pre-applypatch.sample
-rwxr-xr-x  1 tomo  staff   1.6K Nov 11 20:44 pre-commit.sample
-rwxr-xr-x  1 tomo  staff   416B Nov 11 20:44 pre-merge-commit.sample
-rwxr-xr-x  1 tomo  staff   1.3K Nov 11 20:44 pre-push.sample
-rwxr-xr-x  1 tomo  staff   4.8K Nov 11 20:44 pre-rebase.sample
-rwxr-xr-x  1 tomo  staff   544B Nov 11 20:44 pre-receive.sample
-rwxr-xr-x  1 tomo  staff   1.5K Nov 11 20:44 prepare-commit-msg.sample
-rwxr-xr-x  1 tomo  staff   2.7K Nov 11 20:44 push-to-checkout.sample
-rwxr-xr-x  1 tomo  staff   3.6K Nov 11 20:44 update.sample

涉及提交相关的是下面四个:

  • pre-commit - 在 Git 生成 commit 对象前执行
  • prepare-commit-msg - 在 pre-commit 后执行,用以生成默认的提交信息,脚本接收三个参数:
    1. 包含提交信息的临时文件名
    2. 提交的类型,如 message, template, merge, squash
    3. 相关提交的 SHA1,仅在有 -c, -C--amend 参数时提供该参数
  • commit-msg - 在开发者编写提交信息后执行,仅有临时文件名一个参数
  • post-commit - 在 commit-msg 后立马执行,更多做通知用

我们可以用 prepare-commit-msg 对提交信息规范做说明,并用 commit-msg 对规范的执行

进行检查,脚本的非 0 的返回会中断本次提交。

如我们想应用简单的类似 Angular 的 <header> 的格式,可以参考如下的实现。

下面是 prepare-commit-msg 的示例:

python
#!/usr/bin/env python

import sys, os, re
from subprocess import check_output

# Collect the parameters
commit_msg_filepath = sys.argv[1]
if len(sys.argv) > 2:
    commit_type = sys.argv[2]
else:
    commit_type = ''
if len(sys.argv) > 3:
    commit_hash = sys.argv[3]
else:
    commit_hash = ''

print("prepare-commit-msg: File: %s\nType: %s\nHash: %s" % (commit_msg_filepath, commit_type, commit_hash))

msg_spec = '''# Please use follow format
# <type>(<scope>): <short summary>
#  │       │             │
#  │       │             └─⫸ Summary in present tense. Not capitalized. No period at the end.
#  │       │
#  │       └─⫸ Commit Scope: animations|bazel|benchpress|common|compiler|compiler-cli|core
#  │
#  └─⫸ Commit Type: build|ci|docs|feat|fix|perf|refactor|test'''

with open(commit_msg_filepath, 'r+') as f:
    f.write("\n" + msg_spec)

sys.exit(0)  # return non-zero will abort current commit

下面是简单的 commit-msg 示例:

python
#!/usr/bin/env python

import sys, os, re
# Collect the parameters
commit_msg_filepath = sys.argv[1]
print("commit-msg: File: %s" % commit_msg_filepath)

header_pattern = re.compile(r'^(?P<type>\w+)(\((?P<scope>\w+)\))?: .+$')
commit_types = 'build|ci|docs|feat|fix|perf|refactor|test'.split('|')
commit_scopes = 'animations|bazel|benchpress|common|compiler|compiler-cli|core'.split('|')

with open(commit_msg_filepath, 'r') as f:
    commit_msg_header = f.readline().rstrip('\n')  # header line
    print('<header>: %s' % commit_msg_header)
    match = header_pattern.match(commit_msg_header)
    if not match:
        print('commit message does not meet spec')
        sys.exit(1)
    commit_type = match.group('type')
    commit_scope = match.group('scope')
    if commit_type not in commit_types:
        print('invalid <type>')
        sys.exit(1)
    if commit_scope and commit_scope not in commit_scopes:  # scope is optional
        print('invalid <scope>')
        sys.exit(1)

sys.exit(0)

想使用相关的 Git Hooks,可以在目录 .git/hooks 创建对应的文件,文件名为 prepare-commit-msg

commit-msg,并赋予可执行权限。这样在我们进行 git commit 操作时,对应的脚本就会执行。

下图是相关执行示意图,其中不合规范的提交会被中断。

git hooks demo

具体执行过程可以参考 在线执行过程

Git 的提交不会包含 .git 目录,所以对应的 hooks 的改动并不会被提交到仓库中。我们可以在仓库根目录

创建 .githooks 文件夹并将我们实现的代码放到该目录中,通过更改配置或者软连接的方式进行引用:

shell
# use config
git config core.hooksPath .githooks
# OR use soft link
ln -sf .githooks/* .git/hooks

当然这些都是客户端的校验,开发者可以完全忽视这样的一些 Git Hooks 的配置并引入不合规范的提交,

这种情况下我们可以使用服务端校验进行处理,或者引入一些 CI 工具或使用 GitHub Action 进行校验。

7.2 Author & Committer

Git 中,Author 表示原始纂写该提交的作者,Committer 表示应用该提交的人,如合并 Pull Request

的项目管理员。如果是个人开发者或只使用单个 Git 平台服务(如 GitHub、BitBucket 等),我们一般

不需要对作者进行特别的配置。但如果使用多个 Git 平台或者有公司内部要求,我们可能需要针对不同的仓库

设置不同的用户及邮箱,比如全局可以设置个人的 GitHub 账号,企业内部仓库设置企业邮箱等。

shell
# 全局默认配置
git config --global user.email "<github email>"
git config --global user.name "<github username>"
# 企业内部仓库
git config user.email "<enterprise email>"
git config user.name "<real name>"

7.3 Changed files

我们所有的提交,核心的其实我们提交的文件。不同的提交涉及的文件可多可少,一般遵循以下一些原则:

  • 提交前使用 git diff 查看文件的改动,使用 git add 添加期望进入提交的文件, 使用 git status 查看文件状态,最终使用 git commit 进行提交
  • 单次提交仅提交相关的改动,例如修复两个不同的 bug 应该使用两次独立的提交
  • 鼓励经常性的提交,这样可以更快的分享实现的功能,并且减少代码丢失的风险
  • 在主分支或者协作的功能分支不能提交半成品,提交之前需要进过测试
  • 编译输出,日志,中间产物等,不要引入到提交中,使用 .gitignore 进行相关文件的排除,不同语言 或者操作系统有一些通用的排除配置,参考 github/gitignore
  • 密码、授权凭证、密钥等,不要提交。如 AWS 的 certificate.csv 文件或内容, GCP 的 Service Account 文件等,泄露到公开仓库会导致资源被不法分子使用,造成损失。同时由于 Git 的特性, 想从历史提交中移除这类文件会较为困难,参考 GitHub官方相关文档及描述
  • 对于配置文件(如数据库连接信息等),一般使用配置模板,个人维护本地文件,且该文件在 .gitignore 中配置。或者使用 git update-index --[no-]assume-unchanged <file> 来忽略某些文件的改动
  • 其他一些常用命令(请在明确知道其含义后使用)
    • git reset <file> - 移除被添加的文件(提交之前),reset 命令的其他可以查看帮助文档
    • git clean -f - 移除较多的未被追踪的中间文件
    • git checkout <file> - 回退对某个文件的改动(提交之前)

7.4 Hash & Parent

一般情况,commit hash 及父节点信息我们不需要额外关注,但在特定场景下我们可能需要对 commit

进行修复或者其他处理。在这样的场景下,我们需要理解整个 git 的提交链,每个提交对应的父节点,分支

间的共同祖先,以及本地与远端的差异,尤其涉及 rebase 相关的操作时。同时我们需要在整个提交中

遵循项目使用的工作流模型,使用对应工作流模型中建议的操作(常见的工作流模型参考

Atlassian文档)。

下面是一些实际开发过程中涉及的场景:

  • 在自身的开发分支,某个功能涉及多个提交,在正式合并至主分支前对相关的提交进行整理,可以使用 git rebase -i <commit> 命令,对提交进行合并、废弃、修改提交信息等处理。需要注意的是如果提交 已经发布到远端,需要使用 git push -f 进行覆盖(仅限个人开发分支)。下面是一个简单的例子及相关 命令描述,常见的命令有 pick, reword, fixup, drop 等。
bash
$ git rebase -i 8717c71fc
reword 27e67629b feat: some feature first commit
fixup 7a3f0cd25 feat: some feature second commit
fixup d9a9d7f04 feat: some feature third commit

# Rebase 8717c71fc..d9a9d7f04 onto 8717c71fc (3 commands)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup [-C | -c] <commit> = like "squash" but keep only the previous
#                    commit's log message, unless -C is used, in which case
#                    keep only this commit's message; -c is same as -C but
#                    opens the editor
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
# .       create a merge commit using the original merge commit's
# .       message (or the oneline, if no original merge commit was
# .       specified); use -c <commit> to reword the commit message
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
  • 在一些 Git 工作流模型中,使用 git pull --rebase 对本地提交进行更新
  • 原则上禁止对主分支等进行 git push -f 操作,涉及需要回退的,使用 git revert <commit>
  • 涉及多分枝代码同步,可以使用 git cherry-pick 命令

7.5 Exercise

基于课程中7.1.1 自动化校验commit message方案, 实现README.md 中提交信息规范的git-hook的实现。

© 2025 Datawhale. All Rights Reserved.