什么是钩子(Hooks)
钩子是git提供的一个扩展机制,允许用户在某个重要操作的【前&后】执行一段程序(可以是脚本,当然也可以是二进制可执行文件),以此来触发一个外部事件。利用这个机制,可以用来做合规性检查、触发持续集成、自动化测试等。
依照不同分类规则,钩子程序可以分为:
按执行位置分类:
- 客户端钩子:在本地触发和执行;
- 服务端钩子:在服务器触发和执行;
按触发时机分类:
- 前置钩子(pre-hooks):在某个操作前被执行,执行失败(返回非零值)则取消用户操作,钩子名称一般以
pre-
开头;
- 后置钩子(post-hooks):在某个操作后被执行;无论是否执行成功都不影响用户操作;
目前git(v2.41.0)中支持的钩子包括:
- applypatch-msg
- pre-applypatch
- post-applypatch
- pre-commit
- pre-merge-commit
- prepare-commit-msg
- commit-msg
- post-commit
- pre-rebase
- post-checkout
- post-merge
- pre-push
- pre-receive(服务端)
- update(服务端)
- proc-receive(服务端)
- post-receive(服务端)
- post-update(服务端)
- reference-transaction
- push-to-checkout(服务端)
- pre-auto-gc
- post-rewrite
- sendemail-validate
- fsmonitor-watchman
- p4-changelist
- p4-prepare-changelist
- p4-post-changelist
- p4-pre-submit
- post-index-change
想了解每个hook的触发时机,可以阅读git help hooks
文档,这里不做赘述。
怎么用
知道了有哪些钩子,以及触发时机,下面我们看看如何添加一个钩子程序
客户端:
客户端的钩子存放在.git/hooks/
目录下,需要用到哪个钩子,只需要添加一个对应的文件就可以了,注意钩子文件的名称,需要和上一节中的名称保持一致,同时为钩子文件增加执行权限。
类unix系统,可以执行chmod u+x <hook-file>
增加执行权限;
服务端:
至于服务端,就比较麻烦一点,一般的git服务器软件都提供了web配置页面,供用户定制,一般不会通过修改脚本文件的方式。下面分别是github和gitlab的webhook配置页面:
不过,如果你通过标准的ssh服务来搭建git服务,而不是复杂的gitlab或github服务,也可以通过修改.git/hooks
目录下的脚本来实现钩子。
举个例子
这里我们利用post-receive
钩子来关闭gitlab issue的功能。这是一个服务器端的钩子,在收到客户端的push后触发。我们的钩子脚本会检查是否是一个bugfix提交,同时检查里面是否包含issue的编号,如果有,修改对应的issue状态为close。我们这里用的语言是python,代码如下:
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
|
#!/usr/bin/env python3
# post-receive
# run: pip install gitpython requests
import git
import logging
import sys
import re
import requests
# change THIS to your own token and project id
GITLAB_TOKEN = ""
GITLAB_PROJECT_ID = ""
logging.basicConfig(level=logging.INFO, filename='/tmp/post-recv.log', filemode = 'a', format = '%(asctime)s - %(levelname)s: %(message)s')
def close_issue(IID):
GITLAB_URL = f"https://gitlab.com/api/v4/projects/{GITLAB_PROJECT_ID}/issues/{IID}?state_event=close"
HEADERS = {"PRIVATE-TOKEN": GITLAB_TOKEN}
result = requests.put(GITLAB_URL, headers=HEADERS)
if result.status_code == 200:
# logging.info(f"issue {IID} closed")
else:
logging.error(f"failed with status:{result.status_code}")
def extract_issue_id(msg):
if msg.startswith("fix: "):
match = re.search(r'#(\d+)\s+', msg)
if match:
return match.group(1)
else:
return ""
else:
return ""
repo = git.Repo('.')
for oneline in sys.stdin:
# logging.info(oneline)
# oneline format: HASH1 HASH2 refname
# example: 50d688ea0b85ac401450bbeae897fd2e38ed1b21 dc0f390584b057c11a865c486b293ff593279e13 refs/heads/master
record = re.split(r'\s+', oneline)
str = record[0] + ".." + record[1]
for one_commit in repo.iter_commits(str):
# logging.info(one_commit.message)
iid = extract_issue_id(one_commit.message)
if iid != "":
logging.info(iid)
close_issue(iid)
|
#!/usr/bin/env python3
# post-receive
# run: pip install gitpython requests
import git
import logging
import sys
import re
import requests
# change THIS to your own token and project id
GITLAB_TOKEN = ""
GITLAB_PROJECT_ID = ""
logging.basicConfig(level=logging.INFO, filename='/tmp/post-recv.log', filemode = 'a', format = '%(asctime)s - %(levelname)s: %(message)s')
def close_issue(IID):
GITLAB_URL = f"https://gitlab.com/api/v4/projects/{GITLAB_PROJECT_ID}/issues/{IID}?state_event=close"
HEADERS = {"PRIVATE-TOKEN": GITLAB_TOKEN}
result = requests.put(GITLAB_URL, headers=HEADERS)
if result.status_code == 200:
# logging.info(f"issue {IID} closed")
else:
logging.error(f"failed with status:{result.status_code}")
def extract_issue_id(msg):
if msg.startswith("fix: "):
match = re.search(r'#(\d+)\s+', msg)
if match:
return match.group(1)
else:
return ""
else:
return ""
repo = git.Repo('.')
for oneline in sys.stdin:
# logging.info(oneline)
# oneline format: HASH1 HASH2 refname
# example: 50d688ea0b85ac401450bbeae897fd2e38ed1b21 dc0f390584b057c11a865c486b293ff593279e13 refs/heads/master
record = re.split(r'\s+', oneline)
str = record[0] + ".." + record[1]
for one_commit in repo.iter_commits(str):
# logging.info(one_commit.message)
iid = extract_issue_id(one_commit.message)
if iid != "":
logging.info(iid)
close_issue(iid)
上面的脚本中,需要修改GITLAB_TOKEN
和GITLAB_PROJECT_ID
,在你的项目中创建一个访问的TOKEN(Settings > Access Tokens),项目ID在Settings > General页面可以看到。修改完成后,将上面的脚本放到服务器端your-project.git/hooks/post-receive
中,并添加执行权限chmod +x your-project.git/hooks/post-receive
。这个钩子程序需要用到python3、pip3以及三方库:pip3 install requests gitpython
然后在客户端增加一个提交记录,在提交的消息中包含git commit -m "fix: #1 issue 1 fixed"
,这里以fix:
开头,同时issue编号是以#
开头的数字。
这样你在客户端执行git push
的时候,就会在服务端触发post-receive
脚本,关闭对应的issue。上面的logging是为了调试使用,调试通过后,建议关闭日志打印。这个程序也能处理多个commit记录,一次性push到git服务器的场景。
这个脚本也可以扩展,用来修改Jira/Github等平台的issue状态;
局限
.git/hooks
目录下的钩子程序在执行git clone
时,并不会被下载到客户端,因此对于本地钩子需要通过其他方式来存储;比较好的方式是将这些钩子程序保存到git库上,可以单独存放在一个目录,例如.githooks/
目录下,克隆仓库完成后,执行一下git config --local core.hooksPath .githooks/
,将hooks对应的目录设置到新的目录下;
- gitlab/github等服务端默认的webhook需要自建一个HTTP服务器,来实现事件通知;另外存放在服务器上的
your-project.git/hooks/
下的服务器端的钩子程序,并没有提供web页面以供修改,需要登陆到git服务器上,手动修改对应的钩子文件,存在一定的安全隐患;
- 对于钩子程序中用到的基础软件、依赖包等,需要提前在git服务器上安装,否则无法执行;
注意事项
- 保持钩子程序代码精简,快速结束,否则严重影响git效率;
- 仔细阅读每个钩子触发机制、输入参数、标准输入、返回值要求等信息,避免出现场景遗漏;
- 选择一种你熟悉的语言来编写,常见的shell、python、nodejs、ruby、golang、C等随你选。如果是我的话,优选shell和python,开发效率高;
参考
可以订阅关心的各种类型的事件,同时需要提供一个HTTP服务地址,用于接收事件,这个服务器需要自建。