一个维护版本日志整洁的Git提交规范

1 关于提交日志规范

良好的Commit Message有利于代码审查,能更快速查找变更记录,并且可以直接生成Change log。 Commit Message的写法规范:conventional-changelog 为了规范代码提交的说明,这里我们使用angular的规范写法:

1
2
3
4
5
<type>(<scope>): <subject>
<空行>
<body>
<空行>
<footer>

其中 head((): )是必须的,body和footer是可选的。 如果只需要输入header,可以直接使用:

git commit -m

命令提交。 如果需要输入body、footer这样的多行日志,需要输入:

git commit

跳出文本编辑器进行编写。

1.1 Header

1.1.1 Type

commit的类别,包涵以下七种:

  • feat:新功能(feature)
  • fix:修补bug
  • docs:文档(documentation)
  • style: 格式(不影响代码运行的变动)
  • refactor:重构(即不是新增功能,也不是修改bug的代码变动)
  • test:增加测试
  • chore:构建过程或辅助工具的变动

1.1.2 Scope

commit的影响范围,比如会影响到哪个模块/性能/哪一层(业务层,持久层,缓存,rpc),如果是特性代码,可以写特性名称

1.1.3 Subject

commit的简短描述,不超过50个字符。

  • 使用现在式,祈使句
  • 第一个字母无需大写
  • 结尾不用加句号

1.2 Body

跟subject一样,使用现在式,祈使句。应该说明提交代码的动机,以及跟前一个版本的对比。

Foot包含可以包含以下信息:

1.3.1 描述重大变更的信息

以 BREAKING CHANGE 开头,后面是变更的具体描述,如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
BREAKING CHANGE: isolate scope bindings definition has changed and
the inject option for the directive controller injection was removed.

To migrate the code follow the example below:

Before:

scope: {
myAttr: 'attribute',
myBind: 'bind',
myExpression: 'expression',
myEval: 'evaluate',
myAccessor: 'accessor'
}

After:

scope: {
myAttr: '@',
myBind: '@',
myExpression: '&',
// myEval - usually not useful, but in cases where the expression is assignable, you can use '='
myAccessor: '=' // in directive's template change myAccessor() to myAccessor
}

The removed `inject` wasn't generaly useful for directives so there should be no code using it.

1.3.2 关闭JIRA

如:

Closes DB-1001, DB1002

1.4 一些 Commit Message 例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
feat($browser): onUrlChange event (popstate/hashchange/polling)

Added new event to $browser:
- forward popstate event if available
- forward hashchange event if popstate not available
- do polling when neither popstate nor hashchange available

Breaks $browser.onHashChange, which was removed (use onUrlChange instead)

---------

fix($compile): couple of unit tests for IE9

Older IEs serialize html uppercased, but IE9 does not...
Would be better to expect case insensitive, unfortunately jasmine does
not allow to user regexps for throw expectations.

Closes #392
Breaks foo.bar api, foo.baz should be used instead

---------

eat(directive): ng:disabled, ng:checked, ng:multiple, ng:readonly, ng:selected

New directives for proper binding these attributes in older browsers (IE).
Added coresponding description, live examples and e2e tests.

Closes #351

---------

feat($compile): simplify isolate scope bindings

Changed the isolate scope binding options to:
- @attr - attribute binding (including interpolation)
- =model - by-directional model binding
- &expr - expression execution binding

This change simplifies the terminology as well as
number of choices available to the developer. It
also supports local name aliasing from the parent.

BREAKING CHANGE: isolate scope bindings definition has changed and
the inject option for the directive controller injection was removed.

To migrate the code follow the example below:

Before:

scope: {
myAttr: 'attribute',
myBind: 'bind',
myExpression: 'expression',
myEval: 'evaluate',
myAccessor: 'accessor'
}

After:

scope: {
myAttr: '@',
myBind: '@',
myExpression: '&',
// myEval - usually not useful, but in cases where the expression is assignable, you can use '='
myAccessor: '=' // in directive's template change myAccessor() to myAccessor
}

The removed `inject` wasn't generaly useful for directives so there should be no code using it.

如果是特性开发,则可以这样:

feat(短视频播放优化): 全屏播放动画效果优化

2 提交日志自动校验

在NodeJS项目中,我们可以通过使用 ghooks + validate-commit-msg进行提交日志校验,校验不通过的将被拒绝提交。原理是通过NodeJs项目编译,把NodeJS的校验脚本写到.git/hooks/目录下的勾子文件中,这样每次执行git commit命令都会执行这个校验,不过这种方式依赖于Node环境,并且每个Git项目都需要引入对应的npm模块进行编译,对于比较多微服务项目的情况来说使用不太方便。 为此,编写了git-hook-maven-plugin插件, 该插件有如下特点:

  • 很方便的在项目中自定义团队的工作流, 把自定义钩子钩子脚本纳入git管理类, 方便团队共享;
  • 把钩子脚本的安装集成到Maven的生命周期中的某个阶段, 方便的通过项目编译自动安装或者升级脚本;
  • 提供通用的内置脚本, 方便钩子的配置, 目前只提供了validate-commit-message钩子脚本, 用于提交日志的规范, 遵循AngularJS Git Commit Message Conventions的格式。

具体安装参考说明文档。 配置完这个,当我们通过maven编译完项目之后,下一次提交代码,如果提交日志不符合规范,则会报错:

1
2
3
4
➜  project-1 git:(master) ✗ git commit -m "test"

Commit log error: First commit message line (commit header) does not follow format: type(scope): subject
- Refer commit guide: https://docs.google.com/document/d/1QrDFcIiPjSLDn3EL15IJygNPiHORgU1_OOAqWjiDU5Y/edit#

3 特性开发提交压缩合并

对于独自完成的特性,可能在开发过程中会产生多个提交,为了让提交整洁,需要对这个特性的所有提交进行压缩合并。我们先看看压缩合并之前的代码:

1
2
3
4
5
6
7
* be6e32d (HEAD->master)feat(测试提交): 修改第二个文件
* 4a7615e feat(测试提交): 修改第一个文件
* 721064e feat(测试提交): 提交第四个文件
* e20968e feat(测试提交): 提交第三个文件
* b7160b3 feat(测试提交): 提交第二个文件
* 0c90fcl feat(测试提交): 提交第一个文件
* e618321 fix(页面展示): 展示错误修复

如图,可以发现压缩合并测试特性有多个提交,可以进行合并,现在准备把e618321前面的提交都进行合并,执行git rebase -i命令进行压缩合并:

git rebase -i e618321

其中命令后面的hash值是压缩合并开始的那个提交的hash值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
pick 0c90fcl feat (测试提交): 提交第一个文件
pick b7160b3 feat(测试提交): 提交第二个文件
pick e20968e feat(测试提交): 提交第三个文件
pick 721064e feat(测试提交): 提交第四个文件
pick 4a7615e feat(测试提交): 修改第一个文件
pick be6e32d feat(测试提交): 修改第二个文件

# Rebase e618321..be6e32d onto e618321 (6 command(s))
#
# Commands:
# p, pick = use commit
# r, reword = use commit, but edit the commit message
# e, edit = use commit, but stop for amending
# s, squash = use commit, but meld into previous commit
# f, fixup = like "squash", but discard this commit's log message
# x, exec = run command (the rest of the line) using shell
# d, drop = remove commit
#
# 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.
#
# Note that empty commits are commented out

注意:这个界面里面的提交是按照提交时间顺序往下排的,最新提交的在最后面,跟git lg的相反,平时查看日志是最新提交的排在最前面。 我们把低2行到6行改为 s(squash),第一个行改为 r(reword),把第2到6行合并到第一个提交里面,调整下提交信息:

1
2
3
4
5
6
r 0c90fcl feat (测试提交): 提交第一个文件
s b7160b3 feat(测试提交): 提交第二个文件
s e20968e feat(测试提交): 提交第三个文件
s 721064e feat(测试提交): 提交第四个文件
s 4a7615e feat(测试提交): 修改第一个文件
s be6e32d feat(测试提交): 修改第二个文件

然后输 :wq 保存并退出编辑模式继续处理。 如果遇到有冲突,需要解决完冲突之后,修改冲突的提交日志。 最后是一个压缩合并之后的提交日志确认界面,看看第一行的日志是否符合我们的需求,没有问题则输入 :wq 保存并退出编辑模式。

1
2
3
4
5
6
7
8
9
10
11
# This is acombination of 7commits.
# The first commit'smessage is:
feat(测试提交): 测试代码

# This is the 2nd commit message:

feat(测试提交): 提交第二个文件

# This is the 3rd commit message:

feat(测试提交): 提交第三个文件

这样我们就合并完成了,再次查看提交日志,发现已经合并程了一个提交:

1
2
* eb0121a (HEAD->master) feat(测试提交): 测试代码
* e618321 fix(页面展示): 展示错误修复

注:git lg中 lg = log –graph –oneline –decorate,特别注意,本次演示代码直接在master上面进行,实际上需要在 feature-xxx 分支上面进行。 提交代码和压缩合并的原则:

开发过程中的提交代码原则: 尽量减少commit的次数; 在push之前,需要把本次特性所有的代码都压缩成一个提交; 在提交代码审查之前,最好把本次特产压缩成一个提交

4 合并master代码使用 rebase

假设我们开发一个新的特性,并且已经把代码进行了压缩合并,提交日志如下:

1
2
3
* 69b4ffb (HEAD -> feature-test) feat(测试特性): 测试特性代码
* eb0121a (HEAD->master) feat(测试提交): 测试代码
* e618321 fix(页面展示): 展示错误修复

而此时,master代码也有了新的修复代码提交:

1
2
3
* 69b4ffb (HEAD -> master) fix(缓存): 修复产品接口缓存bug
* eb0121a (HEAD->master) feat(测试提交): 测试代码
* e618321 fix(页面展示): 展示错误修复

为了保持合并之后,我们当前特性点仍然在提交日志的最新位置,达成效果如下: 我们使用rebase进行合并代码:

git rebase master

合并之后效果如下,我们当前特性分支的提交仍然在最前面:

1
2
3
4
* 69b4ffb (HEAD -> feature-test) feat(测试特性): 测试特性代码
* 69b4ffb (HEAD -> master) fix(缓存): 修复产品接口缓存bug
* eb0121a (HEAD->master) feat(测试提交): 测试代码
* e618321 fix(页面展示): 展示错误修复

rebase代码原则:

特性分支合并master代码使用 rebase,总让当前特性处于最近提交; 如果一个特性有多个同事开发,rebase之前记得让其他同事提交所有代码,rebase之后记得让其他同事强制更新本地代码;

References

Git 提交的正确姿势:Commit message 编写指南 你可能会忽略的 Git 提交规范 Git钩子:自定义你的工作流 用 Node.js 写前端自己的 Git-hooks https://gist.github.com/jasonrobertfox/8057124 Bash in Linux v.s Mac OS GitUntrackedFilesMojo

arthinking wechat
欢迎关注itzhai公众号