git hook 的无限畅想与简单落地总结

背景

好的 commit message 是至关重要的,如果随意编写 log,带来的后果可小可大,但是无论大小都影响了开发的效率和回朔的难度,所以有必要进行 log 规范化检查。而检查的入口可以从 git hook 切入,而 git hook 却又有无限的遐想。

关于 git commit 问题可以参考下面几个帖子:

Augular git commit 提交规范

如何写好 Git commit log?

跑远了,我们下面先介绍下 git hook 吧。

git hook

和其它版本控制系统一样,Git 能在特定的重要动作发生时触发自定义脚本。 有两组这样的钩子:客户端的和服务器端的。 客户端钩子由诸如提交和合并这样的操作所调用,而服务器端钩子作用于诸如接收被推送的提交这样的联网操作。 你可以随心所欲地运用这些钩子。

钩子都被存储在 Git 目录下的 hooks 子目录中。 也即绝大部分项目中的 .git/hooks ,默认存在的都是示例,其名字都是以 .sample 结尾,如果你想启用它们,得先移除这个后缀。把一个正确命名且可执行的文件放入 Git 目录下的 hooks 子目录中,即可激活该钩子脚本。 这样一来,它就能被 Git 调用。

关于各种详细的 hook 类型可以参考官方文档《自定义 Git - Git 钩子》。

了解了这些 hook 钩子,你就可以真的为所欲为了,你可以用来检查消息、检查代码,可以用来触发任意流程,譬如自动规范检查等等,只能说想象空间巨大无比。

commit msg 格式自动检查实战

虽然有很多现成的 hook 可用,但是这里还是给出一个简单的例子演示下,这里实现一个提交 message 格式的简单检查,要求提交消息单行且分两部分,且有一定的字数限制(实际你可以任意 YY 规则)。

编写 commit-msg 脚本

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
#!/usr/bin/env python
# coding=utf-8
#
# commit msg check
import sys
import re
import io

if hasattr(sys.stdout, 'buffer'):
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')

TIPS_INFO = '''
不符合commit规范,提交失败(当前状态等于没做刚刚的commit操作)!

commit规范:
类型 详细消息

规范样例:
git commit -m "xxxxx xxxxxxxxxxxxx"

!!!!提交失败!!!!
'''

def check_commit_line1_format(msg):
regOther = r'\S{5,} (.){10,100}'
matchObj = re.match(regOther, msg)
return matchObj

if __name__=="__main__":
with open(sys.argv[1], 'r') as f:
for line in f:
if (check_commit_line1_format(line)):
sys.exit(0)
else:
print(TIPS_INFO)
sys.exit(1)

既然编写好了 commit hook 脚本,那新问题就来了,本地 hook 在项目的.git目录下,.git目录又不受版本控制,所以在团队推广时,你不可能让大家主动去放这个文件,一方面可能会放错,另一方面可能有些人压根就不放,爱理不理,故而需要将这件事做成自动的,所以选择在编译项目时为拷贝切入点(因为你拽下来的项目一定会编译的)。对于 android 项目来说,我们可以使用 gradle 编写一个小任务来做这件事,具体如下:

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
/**
* git-hook-copy.gradle 文件
*
* 本地项目 git hook 自动拷贝脚本
* 用法:
* apply from: 'git-hook-copy.gradle'
*/

/**
* 紧急开关
*/
def forbid = false

project.afterEvaluate {
if (forbid) {
preBuild.dependsOn 'resetGitHookConfig'
} else {
preBuild.dependsOn 'prepareGitHookConfig'
}
}

task prepareGitHookConfig(type: Copy) {
from getConfigFile()
into getGitHookDir()
}

task resetGitHookConfig {
doFirst {
File file = getGitHookFile('commit-msg')
if (file != null) {
file.delete()
}
}
}

def getGitHookFile(fileName) {
def dirPath = getGitHookDir()
if (dirPath != null && dirPath.length() > 0) {
def file = new File(dirPath, fileName)
if (file.exists()) {
return file
}
}
return null
}

def getConfigFile() {
File configFile = new File(project.rootDir, "git-hook/commit-msg")
if (configFile.exists()) {
return configFile.absolutePath
}
return null
}

def getGitHookDir() {
File gitHookDir = new File(project.rootDir, ".git/hooks/")
if (!gitHookDir.exists()) {
println("Your project can't find .git directory in the ${project.rootDir.absolutePath}," +
" please ensure it have been tracked by git VCS!")
return null
}
return gitHookDir.absolutePath
}

上面脚本有两个任务,一个 reset,一个 config。reset 会将.git hook目录下的规则删掉,等于没有规则;config 是把项目根目录下 git-hook 目录下的 commit-msg hook 脚本复制到.git hook目录下,这里不用判断是否已经存在文件,直接覆盖即可,因为 gradle task 天生支持 UPDATE 机制,而且我们需要在修改 commit-msg 文件后自动覆盖,所以不建议判断.git hook下是否已经存在。

至此,我们可以进行一波操作了,譬如在命令行提交代码,你会看到如下提示:

git-commit-check

修复 log 格式后再进行 commit 即可。对于 IDE VCS 也会提示不符合规范,对于 git 乌龟客户端也会提示,所以请放心使用,不局限 git 工具的。

总结

上面简单介绍和实战了一个小的 git hook 操作,感兴趣你可以无限想象,和你的 checkstyle 什么的,各种检查什么的结合起来都可以,反正师傅领进门,修行靠自己,需求靠团队。

工匠若水 wechat
关注码农每日一题,每天都有短小精干的收获.
您的收获就是对我最大的鼓励
0%