git

git hooks

git hooks

Posted by WTJ on June 1, 2019

git hooks

git hooks是一些自定义的脚本,用于控制git工作的流程,分为客户端钩子和服务端钩子。

一、客户端钩子

客户端钩子分为很多种。 下面把它们分为:提交工作流钩子、电子>邮件工作流钩子和其它钩子。 主要介绍提交工作流钩子:

  • pre-commit
  • prepare-commit-msg
  • commit-msg
  • post-commit

hooks_two

pre-commit(预先提交)

这个钩子由git commit调用,可以使用–no-verify选项绕过它。它不接受任何参数,并在获取建议的提交日志消息和进行提交之前被调用。从这个脚本中退出非零状态会导致git commit命令在创建提交之前中止。

默认的pre-commit挂钩(如果启用)捕获带有尾随空白的行的引入,并在找到此类行时中止提交。

如果命令不会打开编辑器来修改提交消息,则使用环境变量 GIT_EDITOR=:调用所有git commit挂钩。

当启用hooks.allownonascii配置选项unset或设置为false时,默认的pre-commit挂钩将阻止使用非ASCII文件名。

在键入提交信息前运行。 它用于检查即将提交的快照。

例如,

  • 检查是否有所遗漏
  • 确保测试运行
  • 以及核查代码

如果该钩子以非零值退出,Git 将放弃此次提交,不过你可以用 git commit --no-verify 来绕过这个环节。

你可以利用该钩子,来检查:

  • 代码风格是否一致(运行类似 lint 的程序)
  • 尾随空白字符是否存在(自带的钩子就是这么做的)
  • 新方法的文档是否适当。

pre-merge-commit(合并前提交)

这个钩子由git merge[1]调用,可以使用–no-verify选项绕过它。它不接受任何参数,并在合并成功执行之后和获取建议的提交日志消息以进行提交之前调用。从这个脚本中退出非零状态会导致Git合并命令在创建提交之前中止。

如果启用了pre-merge-commit挂钩,则默认的预合并提交挂钩将运行pre-commit挂钩。

如果命令不会调出编辑器来修改提交消息,则使用环境变量GIT_EDITOR=:调用此挂钩。

如果无法自动执行合并,则需要解决冲突并单独提交结果(参见git merge)。此时,将不会执行此挂钩,但如果启用了pre-commit挂钩,则会执行它。

prepare-commit-msg(准备提交消息)

git commit在准备默认日志消息之后,在启动编辑器之前调用此钩子。

它需要一到三个参数。第一个是包含提交日志消息的文件的名称。第二个是提交消息的来源,可以是:message(如果给出了-m或-F选项);template(如果给出了-t选项或配置选项commit.template);merge(如果提交是合并或.git/MERGE_MSG文件);squash(如果.git/SQUASH_MSG文件存在);或commit,接着是提交SHA-1(如果是-c,-C)或者–amend 选项)。

如果退出状态为非零,则git commit将中止。

钩子的目的是就地编辑消息文件,而–no-verify选项不禁止它。非零退出意味着钩子失败,并中止提交。它不应该用作预提交挂钩的替换。

Git附带的prepare-commit-msg钩子示例删除了commit模板注释部分中的帮助消息。

在启动提交信息编辑器之前,默认信息被创建之后运行。

它允许你编辑提交者所看到的默认信息。

该钩子接收一些选项:

  • 存有当前提交信息的文件的路径
  • 提交类型和修补提交的提交的 SHA-1 校验。

它对一般的提交来说并没有什么用;然而对那些会自动产生默认信息的提交,如:

  • 提交信息模板
  • 合并提交
  • 压缩提交
  • 修订提交等非常实用

你可以结合提交模板来使用它,动态地插入信息。

#!/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((提交信息)

接收一个参数,此参数即上文提到的,存有当前提交信息的临时文件的路径。

如果该钩子脚本以非零值退出,Git 将放弃提交,因此,可以用来在提交通过前验证项目状态或提交信息。

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(提交后)

这个钩子由git commit和git merge调用,可以使用–no-verify选项绕过它。它接受一个参数,即保存建议的提交日志消息的文件的名称。退出非零状态会导致命令中止。

允许钩子就地编辑消息文件,并可用于将消息规范化为某些项目标准格式。它还可用于在检查消息文件后拒绝提交。

默认的commit-msg hook在启用时检测到重复的Signed-off-by行,如果找到一行,则中止提交。

在整个提交过程完成后运行。

它不接收任何参数,但你可以很容易地通过运行 git log -1 HEAD 来获得最后一次的提交信息。

该钩子一般用于通知之类的事情。

E-mail工作流挂钩

有3个可用的客户端挂钩用于e-mail工作流。当运行 git am 命令时,会调用他们,因此,如果你没有在工作流中用到此命令,可以跳过本节。如果你通过e-mail接收由 git format-patch 产生的补丁,这些挂钩也许对你有用。

1.applypatch-msg(应用程序消息)

这个钩子由git am调用。它只有一个参数,即保存建议的提交日志消息的文件的名称。以非零状态退出会导致git am在应用补丁之前中止。

该挂钩允许在适当位置编辑消息文件,并可用于将消息规范化为某些项目标准格式。检查消息文件后,它也可以用于拒绝提交。

启用后,默认的applypatch-msg挂钩将运行 commit-msg挂钩(如果后者已启用)。

2.pre-applypatch(应用前批处理)

这个钩子由git am调用。它不接受任何参数,并在应用补丁程序之后、提交之前调用。

如果它以非零状态退出,则在应用补丁程序后将不会提交工作树。

它可以用来检查当前的工作树,如果不通过某些测试,则拒绝提交。

默认的pre-applypatch钩子在启用时运行pre-commit钩子(如果后者已启用)。

3.post-applypatch(应用程序批处理后)

这个钩子由git am调用。它不接受任何参数,在应用补丁程序并提交之后调用。

这个钩子主要用于通知,不能影响git am的结果。

其他客户端挂钩

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或git switch时,将调用此挂钩。钩子有三个参数:前一个HEAD的ref,新HEAD的ref(可能已经更改,也可能没有更改)和一个标志,指示签出是分支签出(更改分支,flag=1)还是文件签出(从索引中检索文件,flag=0)。此挂钩不会影响git switch或git checkout的结果。

它也在git clone[1]之后运行,除非使用–no-checkout(-n)选项。给钩子的第一个参数是空ref,第二个参数是新头的ref,标志总是1。同样,对于git worktree add,除非–no-checkout签出。

此钩子可用于执行存储库有效性检查、自动显示与前一个HEAD的差异(如果不同)或设置工作目录元数据属性。

二、服务端钩子

除了客户端钩子,作为系统管理员,你还可以使用若干服务器端的钩子对项目强制执行各种类型的策略。 这些钩子脚本在推送到服务器之前和之后运行。 推送到服务器前运行的钩子可以在任何时候以非零值退出,拒绝推送并给客户端返回错误消息,还可以依你所想设置足够复杂的推送策略。

钩子包括:

  • pre-receive
  • update
  • post-receive。

pre-receive(预先接收)

这个钩子由git rebase调用,可用于防止分支重新定位。可以使用一个或两个参数调用钩子。第一个参数是派生序列的上游。第二个参数是正在重设基的分支,重设基当前分支时不设置该参数。

处理来自客户端的推送操作时,最先被调用的脚本是 pre-receive。 它从标准输入获取一系列被推送的引用。如果它以非零值退出,所有的推送内容都不会被接受。 你可以用这个钩子阻止对引用进行非快进(non-fast-forward)的更新,或者对该推送所修改的所有引用和文件进行访问控制。

update(更新)

update 脚本和 pre-receive 脚本十分类似,不同之处在于它会为每一个准备更新的分支各运行一次。 假如推送者同时向多个分支推送内容,pre-receive 只运行一次,相比之下 update 则会为每一个被推送的分支各运行一次。 它不会从标准输入读取内容,而是接受三个参数:引用的名字(分支),推送前的引用指向的内容的 SHA-1 值,以及用户准备推送的内容的 SHA-1 值。 如果 update 脚本以非零值退出,只有相应的那一个引用会被拒绝;其余的依然会被更新。

post-receive。(接收后)

post-receive 挂钩在整个过程完结以后运行,可以用来更新其他系统服务或者通知用户。 它接受与 pre-receive 相同的标准输入数据。 它的用途包括给某个邮件列表发信,通知持续集成(continous integration)的服务器, 或者更新问题追踪系统(ticket-tracking system) —— 甚至可以通过分析提交信息来决定某个问题(ticket)是否应该被开启,修改或者关闭。 该脚本无法终止推送进程,不过客户端在它结束运行之前将保持连接状态, 所以如果你想做其他操作需谨慎使用它,因为它将耗费你很长的一段时间。

三、git hooks 安装

当你用git init初始化一个新版本库时,Git 默认会在这个目录中放置一些示例脚本,进入.git/hooks后会看到一些hooks的官方示例,他们都是以.sample结尾的文件名。

四、git hooks存储位置

git hooks被存储在Git目录下的.hooks子目录中,即绝大部分项目中的.git/hooks

../project/.git/hooks

五、git hooks示例

.git/hooks目录下的示例脚本除了本身可以被调用外,它们还透露了被触发时所传入的参数。

所有的示例都是shell脚本,其中一些还混杂了Perl代码,不过,任何正确命名的可执行脚本都可以正常使用 —— 你可以用Ruby或 Python,或任何你熟悉的语言编写它们。

cd .git/hooks/

git init后生成的示例脚本,如下图

applypatch-msg.sample     pre-merge-commit.sample
commit-msg.sample         pre-push.sample
fsmonitor-watchman.sample pre-rebase.sample
post-commit               pre-receive.sample
post-update.sample        prepare-commit-msg.sample
pre-applypatch.sample     push-to-checkout.sample
pre-commit                update.sample
pre-commit.sample

六、调用 git hooks

Git 默认会放置一些脚本样本在这个目录中,除了可以作为挂钩使用,这些样本本身是可以独立使用的。所有的样本都是shell脚本,其中一些还包含了Perl的脚本。不过,任何正确命名的可执行脚本都可以正常使用 ,也可以用Ruby或Python,或其他脚本语言。

上图是git 初始化的时候生成的默认钩子,已包含了大部分可以使用的钩子,但是 .sample 拓展名防止它们默认被执行。为了安装一个钩子,你只需要去掉 .sample 拓展名。或者你要写一个新的脚本,你只需添加一个文件名和上述匹配的新文件,去掉.sample拓展名。把一个正确命名且可执行的文件放入 Git 目录下的 hooks子目录中,可以激活该挂钩脚本,之后他一直会被 Git 调用。

一个简单的 Hooks 例子

使用shell 这里尝试写一个简单的钩子,安装一个 prepare-commit-msg 钩子。去掉脚本的 .sample 拓展名,在文件中加上下面这两行:

#!/bin/sh

echo "# Please include a useful commit message!" > $1

钩子需要能被执行,所以如果你创建了一个新的脚本文件,你需要修改它的文件权限。比如说,为了确保prepare-commit-msg可执行,运行下面这个命令:

钩子需要能被执行,所以如果你创建了一个新的脚本文件,你需要修改它的文件权限。比如说,为了确保prepare-commit-msg可执行,运行下面这个命令:

接下来你每次运行git commit时,你会看到默认的提交信息都被替换了。

内置的样例脚本是非常有用的参考资料,因为每个钩子传入的参数都有非常详细的说明(不同钩子不一样)。

七、钩子的作用域

对于任何Git仓库来说钩子都是本地的,而且它不会随着git clone一起复制到新的仓库。而且,因为钩子是本地的,任何能接触得到仓库的人都可以修改。在开发团队中维护钩子是比较复杂的,因为.git/hooks目录不随你的项目一起拷贝,也不受版本控制影响。一个简单的解决办法是把你的钩子存在项目的实际目录中(在.git外)。这样你就可以像其他文件一样进行版本控制。

作为备选方案,Git同样提供了一个模板目录机制来更简单地自动安装钩子。每次你使用 git init 或 git clone 时,模板目录文件夹下的所有文件和目录都会被复制到.git文件夹。

参考:

Git hooks 在iOS开发的应用