一、简介
git hooks,即git 钩子,定义为能在特定的重要动作发生时触发自定义脚本。
git 的hook分为两种,客户端hooks和服务端hooks。
客户端钩子由诸如提交和合并这样的操作所调用,而服务器端钩子作用于诸如接收被推送的提交这样的联网操作。
官方文档:https://git-scm.com/docs/githooks
二、说明
结构说明
git hooks在git项目中的位置在 /.git/hooks 目录下,初始化时由以下sample组成:
applypatch-msg.sample*
commit-msg.sample*
post-update.sample*
pre-applypatch.sample*
pre-commit.sample*
prepare-commit-msg.sample*
pre-push.sample*
pre-rebase.sample*
pre-receive.sample*
update.sample*
hook说明
钩子都被存储在 Git 目录下的 hooks 子目录中。 也即绝大部分项目中的 .git/hooks 。 当你用 git init 初始化一个新版本库时,Git 默认会在这个目录中放置一些示例脚本。 这些脚本除了本身可以被调用外,它们还透露了被触发时所传入的参数。 所有的示例都是 shell 脚本,其中一些还混杂了 Perl 代码,不过,任何正确命名的可执行脚本都可以正常使用 —— 你可以用 Ruby 或 Python,或任何你熟悉的语言编写它们。 这些示例的名字都是以 .sample 结尾,如果你想启用它们,得先移除这个后缀。
把一个正确命名(不带扩展名)且可执行的文件放入 .git 目录下的 hooks 子目录中,即可激活该钩子脚本。 这样一来,它就能被 Git 调用。
hook类型
1、applypatch-msg
它接收单个参数:包含请求合并信息的临时文件的名字。 如果脚本返回非零值,Git 将放弃该补丁。 你可以用该脚本来确保提交信息符合格式,或直接用脚本修正格式错误。
2、commit-msg
钩子在启动提交信息编辑器之前,默认信息被创建之后运行。 它允许你编辑提交者所看到的默认信息。 该钩子接收一些选项:存有当前提交信息的文件的路径、提交类型和修补提交的提交的 SHA-1 校验。 它对一般的提交来说并没有什么用;然而对那些会自动产生默认信息的提交,如提交信息模板、合并提交、压缩提交和修订提交等非常实用。 你可以结合提交模板来使用它,动态地插入信息。
3、post-update
仅在所有的ref被push之后执行一次。它与post-receive很像,但是不接收旧值与新值。主要用于通知。每个被push的repo都会生成一个参数,参数内容是ref的名称。
4、pre-applypatch
实际上的调用时机是应用补丁之后、变更commit之前。如果以非0的状态退出,会导致变更成为uncommitted状态。可用于在实际进行commit之前检查代码树的状态或用它在提交前检查快照。 你可以用这个脚本运行测试或检查工作区。 如果有什么遗漏,或测试未能通过,脚本会以非零值退出,中断 git am 的运行,这样补丁就不会被提交。
5、pre-commit
钩子在键入提交信息前运行。 它用于检查即将提交的快照,例如,检查是否有所遗漏,确保测试运行,以及核查代码。 如果该钩子以非零值退出,Git 将放弃此次提交,不过你可以用 git commit --no-verify 来绕过这个环节。 你可以利用该钩子,来检查代码风格是否一致(运行类似 lint 的程序)、尾随空白字符是否存在(自带的钩子就是这么做的),或新方法的文档是否适当。
6、prepare-commit-msg
钩子在启动提交信息编辑器之前,默认信息被创建之后运行。 它允许你编辑提交者所看到的默认信息。 该钩子接收一些选项:存有当前提交信息的文件的路径、提交类型和修补提交的提交的 SHA-1 校验。 它对一般的提交来说并没有什么用;然而对那些会自动产生默认信息的提交,如提交信息模板、合并提交、压缩提交和修订提交等非常实用。 你可以结合提交模板来使用它,动态地插入信息
7、pre-push
钩子会在 git push 运行期间, 更新了远程引用但尚未传送对象时被调用。 它接受远程分支的名字和位置作为参数,同时从标准输入中读取一系列待更新的引用。 你可以在推送开始之前,用它验证对引用的更新操作(一个非零的退出码将终止推送过程)。
8、pre-rebase
钩子运行于变基之前,以非零值退出可以中止变基的过程。 你可以使用这个钩子来禁止对已经推送的提交变基。 Git 自带的 pre-rebase 钩子示例就是这么做的,不过它所做的一些假设可能与你的工作流程不匹配。
9、pre-receive(服务器端)
处理来自客户端的推送操作时,最先被调用的脚本是 pre-receive。 它从标准输入获取一系列被推送的引用。如果它以非零值退出,所有的推送内容都不会被接受。 你可以用这个钩子阻止对引用进行非快进(non-fast-forward)的更新,或者对该推送所修改的所有引用和文件进行访问控制。
10、update(服务器端)
update 脚本和 pre-receive 脚本十分类似,不同之处在于它会为每一个准备更新的分支各运行一次。 假如推送者同时向多个分支推送内容,pre-receive 只运行一次,相比之下 update 则会为每一个被推送的分支各运行一次。 它不会从标准输入读取内容,而是接受三个参数:引用的名字(分支),推送前的引用指向的内容的 SHA-1 值,以及用户准备推送的内容的 SHA-1 值。 如果 update 脚本以非零值退出,只有相应的那一个引用会被拒绝;其余的依然会被更新。
11、post-receive(服务器端)
post-receive 挂钩在整个过程完结以后运行,可以用来更新其他系统服务或者通知用户。 它接受与 pre-receive 相同的标准输入数据。 它的用途包括给某个邮件列表发信,通知持续集成(continous integration)的服务器, 或者更新问题追踪系统(ticket-tracking system) —— 甚至可以通过分析提交信息来决定某个问题(ticket)是否应该被开启,修改或者关闭。 该脚本无法终止推送进程,不过客户端在它结束运行之前将保持连接状态, 所以如果你想做其他操作需谨慎使用它,因为它将耗费你很长的一段时间。
12、post-checkout
更新工作树后调用checkout时调用,或者执行 git clone后调用。主要用于验证环境、显示变更、配置环境。在 git checkout 成功运行后,post-checkout 钩子会被调用。你可以根据你的项目环境用它调整你的工作目录。 其中包括放入大的二进制文件、自动生成文档或进行其他类似这样的操作。
13、post-rewrite
本hook在git命令重写(rewrite)已经被commit的数据时调用。除了其携带的参数之外,本hook还从stdin接收信息,信息格式为” ”。post-rewrite 钩子被那些会替换提交记录的命令调用,比如 git commit --amend 和 git rebase(不过不包括 git filter-branch)。 它唯一的参数是触发重写的命令名,同时从标准输入中接受一系列重写的提交记录。 这个钩子的用途很大程度上跟 post-checkout 和 post-merge 差不多。
完整说明详见官方文档:https://git-scm.com/docs/githooks
三、一个简单的 Hooks 例子
使用shell 这里尝试写一个简单的钩子,安装一个prepare-commit-msg钩子。去掉脚本的.sample拓展名,在文件中加上下面这两行:
#!/bin/sh echo "# Please include a useful commit message!" > $1
接下来你每次运行git commit时,你会看到默认的提交信息都被替换了。
内置的样例脚本是非常有用的参考资料,因为每个钩子传入的参数都有非常详细的说明(不同钩子不一样)。
四、脚本语言
git自己生成的默认钩子的脚本大多是shell和Perl语言的,但你可以使用任何脚本语言,只要它们最后能编译到可执行文件。每次脚本中的 #!/bin/sh 定义了你的文件将被如何解析。比如,使用其他语言时你只需要将path改为你的解释器的路径。
比如说,你可以在prepare-commit-msg 中写一个可执行的Python脚本。下面这个钩子和上一节的shell脚本做的事完全一样。
#!/usr/bin/env python import sys, os commit_msg_filepath = sys.argv[1] with open(commit_msg_filepath, 'w') as f: f.write("# Please include a useful commit message!")
注意第一行改成了python解释器的路径。此外,这里用sys.argv[1]而不是$1来获取第一个参数。这个特性非常强大,因为你可以用任何你喜欢的语言来编写Git钩子。
五、钩子的作用域
对于任何Git仓库来说钩子都是本地的,而且它不会随着git clone一起复制到新的仓库。而且,因为钩子是本地的,任何能接触得到仓库的人都可以修改。在开发团队中维护钩子是比较复杂的,因为.git/hooks目录不随你的项目一起拷贝,也不受版本控制影响。一个简单的解决办法是把你的钩子存在项目的实际目录中(在.git外)。这样你就可以像其他文件一样进行版本控制。
作为备选方案,Git同样提供了一个模板目录机制来更简单地自动安装钩子。每次你使用git init 或git clone时,模板目录文件夹下的所有文件和目录都会被复制到.git文件夹。
六、常用钩子有哪些
就像上面说的,那么多钩子我们不是都会用到,下面就介绍几个经常用到的钩子,举例说明一下。
1、客户端 Hooks
客户端钩子只影响它们所在的本地仓库。有许多客户端挂钩,以下把他们分为:提交工作流挂钩、电子邮件工作流挂钩及其他客户端挂钩。
1.提交工作流挂钩
commit操作有 4个挂钩被用来处理提交的过程,他们的触发时间顺序如下:
pre-commit、prepare-commit-msg、commit-msg、post-commit
pre-commit
pre-commit 挂钩在键入提交信息前运行,最先触发运行的脚本。被用来检查即将提交的代码快照。例如,检查是否有东西被遗漏、运行一些自动化测试、以及检查代码规范。当从该挂钩返回非零值时,Git 放弃此次提交,但可以用git commit --no-verify来忽略。该挂钩可以被用来检查代码错误,检查代码格式规范,检查尾部空白(默认挂钩是这么做的),检查新方法(译注:程序的函数)的说明。
pre-commit 不需要任何参数,以非零值退出时将放弃整个提交。这里,我们用 “强制代码格式校验” 来说明。
prepare-commit-msg
prepare-commit-msg 挂钩在提交信息编辑器显示之前,默认信息被创建之后运行,它和 pre-commit 一样,以非零值退出会放弃提交。因此,可以有机会在提交作者看到默认信息前进行编辑。该挂钩接收一些选项:拥有提交信息的文件路径,提交类型。例如和提交模板配合使用,以编程的方式插入信息。提交信息模板的提示修改在上面已经看到了,现在我们来看一个更有用的脚本。在处理需要单独开来的bug时,我们通常在单独的分支上处理issue。如果你在分支名中包含了issue编号,你可以使用prepare-commit-msg钩子来自动地将它包括在那个分支的每个提交信息中。
#!/usr/bin/env python import sys, os, re from subprocess import check_output # 收集参数 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) # 检测我们所在的分支 branch = check_output(['git', 'symbolic-ref', '--short', 'HEAD']).strip() print "prepare-commit-msg: On branch '%s'" % branch # 用issue编号生成提交信息 if branch.startswith('issue-'): print "prepare-commit-msg: Oh hey, it's an issue branch." result = re.match('issue-(.*)', branch) issue_number = result.group(1) with open(commit_msg_filepath, 'r+') as f: content = f.read() f.seek(0, 0) f.write("ISSUE-%s %s" % (issue_number, content))
首先,上面的prepare-commit-msg 钩子告诉你如何收集传入脚本的所有参数。接下来,它调用了git symbolic-ref --short HEAD来获取对应HEAD的分支名。如果分支名以issue-开头,它会重写提交信息文件,在第一行加上issue编号。比如你的分支名issue-224,下面的提交信息将会生成:
ISSUE-224 # Please enter the commit message for your changes. Lines starting # with '#' will be ignored, and an empty message aborts the commit. # On branch issue-224 # Changes to be committed: # modified: test.txt
有一点要记住的是即使用户用-m传入提交信息,prepare-commit-msg也会运行。也就是说,上面这个脚本会自动插入ISSUE-[#]字符串,而用户无法更改。你可以检查第二个参数是否是提交类型来处理这个情况。但是,如果没有-m选项,prepare-commit-msg钩子允许用户修改生成后的提交信息。所以这个脚本的目的是为了方便,而不是推行强制的提交信息规范。如果你要这么做,你需要下面所讲的commit-msg钩子。
commit-msg
commit-msg钩子和prepare-commit-msg钩子很像,但它会在用户输入提交信息之后被调用。这适合用来提醒开发者他们的提交信息不符合你团队的规范。传入这个钩子唯一的参数是包含提交信息的文件名。如果它不喜欢用户输入的提交信息,它可以在原地修改这个文件(和prepare-commit-msg一样),或者它会以非零值退出,放弃这个提交。比如说,下面这个脚本确认用户没有删除prepare-commit-msg脚本自动生成的ISSUE-[#]字符串。
#!/usr/bin/env python import sys, os, re from subprocess import check_output # 收集参数 commit_msg_filepath = sys.argv[1] # 检测所在的分支 branch = check_output(['git', 'symbolic-ref', '--short', 'HEAD']).strip() print "commit-msg: On branch '%s'" % branch # 检测提交信息,判断是否是一个issue提交 if branch.startswith('issue-'): print "commit-msg: Oh hey, it's an issue branch." result = re.match('issue-(.*)', branch) issue_number = result.group(1) required_message = "ISSUE-%s" % issue_number with open(commit_msg_filepath, 'r') as f: content = f.read() if not content.startswith(required_message): print "commit-msg: ERROR! The commit message must start with '%s'" % required_message sys.exit(1)
post-commit
post-commit 挂钩在整个提交过程完成后运行,他不会接收任何参数,但可以运行git log来获得最后的提交信息。总之,该挂钩是作为通知之类使用的。虽然可以用post-commit来触发本地的持续集成系统,但大多数时候你想用的是post-receive这个钩子。它运行在服务端而不是用户的本地机器,它同样在任何开发者推送代码时运行。那里更适合进行持续集成。
提交工作流的客户端挂钩脚本可以在任何工作流中使用,他们经常被用来实施某些策略,但值得注意的是,这些脚本在clone期间不会被传送。可以在服务器端实施策略来拒绝不符合某些策略的推送,但这完全取决于开发者在客户端使用这些脚本的情况。所以,这些脚本对开发者是有用的,由他们自己设置和维护,而且在任何时候都可以覆盖或修改这些脚本,后面讲如何把这部分东西也集成到开发流中。
2.E-mail工作流挂钩
有3个可用的客户端挂钩用于e-mail工作流。当运行 git am命令时,会调用他们,因此,如果你没有在工作流中用到此命令,可以跳过本节。如果你通过e-mail接收由git format-patch 产生的补丁,这些挂钩也许对你有用。
首先运行的是 applypatch-msg挂钩,他接收一个参数:包含被建议提交信息的临时文件名。如果该脚本非零退出,Git 放弃此补丁。可以使用这个脚本确认提交信息是否被正确格式化,或让脚本编辑信息以达到标准化。
下一个在git am 运行期间调用是 pre-applypatch 挂钩。该挂钩不接收参数,在补丁被运用之后运行,因此,可以被用来在提交前检查快照。你能用此脚本运行测试,检查工作树。如果有些什么遗漏,或测试没通过,脚本会以非零退出,放弃此次git am的运行,补丁不会被提交。
最后在git am运行期间调用的是post-applypatch 挂钩。你可以用他来通知一个小组或获取的补丁的作者,但无法阻止打补丁的过程。
3.其他客户端挂钩
pre-rebase
pre-rebase挂钩在衍合前运行,脚本以非零退出可以中止衍合的过程。你可以使用这个挂钩来禁止衍合已经推送的提交对象,pre-rebase挂钩样本就是这么做的。该样本假定next是你定义的分支名,因此,你可能要修改样本,把next改成你定义过且稳定的分支名。
比如说,如果你想彻底禁用rebase操作,你可以使用下面的pre-rebase脚本:
#!/bin/sh # 禁用所有rebase echo "pre-rebase: Rebasing is dangerous. Don't do it." exit 1
每次运行git rebase,你都会看到下面的信息:
pre-rebase: Rebasing is dangerous. Don't do it.
The pre-rebase hook refused to rebase.
内置的pre-rebase.sample脚本是一个更复杂的例子。它在何时阻止rebase这方面更加智能。它会检查你当前的分支是否已经合并到了下一个分支中去(也就是主分支)。如果是的话,rebase可能会遇到问题,脚本会放弃这次rebase。
post-checkout
由git checkout命令调用,在完成工作区更新之后执行。该脚本由三个参数:之前HEAD指向的引用,新的HEAD指向的引用,一个用于标识此次检出是否是分支检出的值(0表示文件检出,1表示分支检出)。也可以被git clone触发调用,除非在克隆时使用参数--no-checkout。在由clone调用执行时,三个参数分别为null, 1, 1。这个脚本可以用于为自己的项目设置合适的工作区,比如自动生成文档、移动一些大型二进制文件等,也可以用于检查版本库的有效性。
最后,在 merge 命令成功执行后,post-merge 挂钩会被调用。他可以用来在 Git 无法跟踪的工作树中恢复数据,诸如权限数据。该挂钩同样能够验证在 Git 控制之外的文件是否存在,因此,当工作树改变时,你想这些文件可以被复制。
七、demo(git commit代码检查)
先说思路,思路是使用pre-commit
官方说明:
This hook is invoked by git-commit[1], and can be bypassed with the --no-verify option. It takes no parameters, and is invoked before obtaining the proposed commit log message and making a commit. Exiting with a non-zero status from this script causes the git commit command to abort before creating a commit.
The default pre-commit hook, when enabled, catches introduction of lines with trailing whitespaces and aborts the commit when such a line is found.
All the git commit hooks are invoked with the environment variable GIT_EDITOR=: if the command will not bring up an editor to modify the commit message.
The default pre-commit hook, when enabled—and with the hooks.allownonascii config option unset or set to false—prevents the use of non-ASCII filenames.
初始化并一个git仓库,这里随便用一个go文件仓库作为demo,代码输出 hello world!!
package main import "fmt" var str = "hello world!!" func main() { fmt.Println(str) }
尝试推送一下,此时可以看到,添加hook前能够正常提交
进入.git/hooks目录下:
拷贝一个 pre-commit.sample并命名为 pre-commit,编辑pre-commit
cp .git/hooks/pre-commit.sample .git/hooks/pre-commit && vi .git/hooks/pre-commit
使用bash随手写一个方法,校验代码中的 “hello”字段,如果存在则输出一个异常信息
#!/bin/bash echo "code check running..." for FILE in `git diff --name-only --cached`; do grep 'hello' $FILE 2>&1 >/dev/null if [ $? -eq 0 ]; then echo $FILE '包含hello,退出commit' exit 1 fi done
尝试运行一下commit,果然报错了
去除代码中的 hello,再次运行一下commit试试
校验通过,可以正常push。
八、小结
根据所需要的需求选用不同的hook类型,然后使用bash shell/Ruby/Python等编写脚本,在git生命周期中添加你需要的逻辑。
git hooks分为客户端和服务端,需要注意所增加配置的影响范围。
使用时需要去除文件.sample后缀名,否则文件不生效。