Git 钩子

2019年12月04日 阅读数:30
这篇文章主要向大家介绍Git 钩子,主要内容包括基础应用、实用技巧、原理机制等方面,希望对大家有所帮助。
  1. 1. 概念概述
    1. 1.1. 安装钩子
    2. 1.2. 脚本语言
    3. 1.3. 钩子的做用域
  2. 2. 本地钩子
    1. 2.1. 预提交钩子 Pre-Commit
    2. 2.2. 准备提交信息钩子 Prepare Commit Message
    3. 2.3. 提交信息钩子 Commit Message
    4. 2.4. 提交后钩子 Post-Commit
    5. 2.5. 切换后钩子 Post-Checkout
    6. 2.6. 预衍合钩子 Pre-Rebase
  3. 3. 服务端钩子
    1. 3.1. 预接收钩子 Pre-Receive
    2. 3.2. 更新钩子 Update
    3. 3.3. 接受后钩子 Post-Receive
  4. 4. 总结
  5. 5. 注释

Git 钩子Git 钩子html

Git 钩子是在一个 Git 仓库中,在每一次特定事件触发时自动运行的脚本。它容许咱们自定义 Git 的内部行为,并在开发的生命周期的关键时间点触发自定义行为。python

经过连接到版本控制的脚本维护钩子经过连接到版本控制的脚本维护钩子git

Git 钩子一般的使用案例包含支持一个提交策略,根据仓库状态替换项目环境,以及实现连续集成的工做流。但因为脚本是能够任意指定的,因此咱们几乎可使用 Git 的钩子来自动化或优化咱们开发工做流的方方面面。github

本文的一开始咱们先从概念上概述一下 Git 钩子是怎么工做的;以后咱们会研究一些使用在本地和服务端仓库的最流行的钩子的使用。shell

概念概述

全部的 Git 钩子都是普通的脚本,只是在仓库的特定时机会被 Git 执行。这是得钩子们的安装和配置十分容易。编程

钩子能够放置在本地或服务端的仓库中,它们仅对各自的仓库行为作出相应并执行。咱们会在下文中具体看一下钩子的类型。应用在本地和服务端的钩子的配置会在余下的章节中进行讨论。安全

安装钩子

钩子存放在每一个 Git 仓库的 .git/hooks 目录。当咱们初始化仓库时,Git 会自动生成此目录并在里面放置一些示例脚本。若是你去看一眼.git/hooks 目录的内容,就会看到以下的文件:服务器

applypatch-msg.sample       pre-push.sample
commit-msg.sample           pre-rebase.sample
post-update.sample prepare-commit-msg.sample pre-applypatch.sample update.sample pre-commit.sample 

这里提供了许多可用的钩子,可是 .simple 的后缀使它们都不能默认执行。想要『安装』某一个钩子,咱们所须要作的仅仅是移除.simple 的扩展名。或者若是你动手撸了一个新脚本,那你能够将脚本添加到路径里,并用上面的文件名来命名你的脚本,记得去掉.simple 后缀哟。session

咱们安装一个简单的 prepare-commit-msg 钩子做为示例。删除文件的 .simple 后缀,并将下面的内容添加到文件中:app

1
2
3
#!/bin/sh

echo "# 请输入一条有用的提交信息!" > $1

钩子文件必须可执行,所以若是咱们是徒手撸的脚本,咱们须要更改文件的权限。例如,若是想要使 prepare-commit-msg 文件可执行,就须要运行下面的命令:

chmod +x prepare-commit-msg

如今咱们应该能在每次运行 git commit 的时候看到默认的提交信息(就是上文脚本里的『请输入一条有用的提交信息』)。咱们将在 准备提交信息 一章仔细研究其工做机制;而如今咱们只须要臭美一下:咱们已经能自定义 Git 内部的方法啦!

内置的样例脚本是很是有用的参考,它们展现了传递给每一个钩子的参数详情(这些参数在钩子之间顺序传递)。

脚本语言

内置的脚本可能是 shell 或 perl 脚本,可是只要咱们编写的脚本可以执行,就可使用任何喜欢的脚本语言。每一个脚本文件的 事务行(shebang line) 定义了该文件的解释方式。所以想要使用其余的脚本语言,咱们只须要改变事务行中解释器的路径就能够了。

举例来讲,咱们能够在 prepare-commit-msg 文件中编写一个可执行的 Python 脚原本替代 shell 脚本。下面的钩子和上一节的 shell 脚本的执行效果彻底一致。

1
2
3
4
5
6
7
#!/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/hooks;或者是当有更新的时候,简单的将其复制粘贴到 .git/hooks 目录中。

在提交建立的过程执行钩子在提交建立的过程执行钩子

另外,Git 还提供一个模板目录的机制来更加便捷的自动安装钩子。模板目录的全部文件和目录在每次使用 git init 和 git clone 时都会被复制进 .git 目录里。

下文所述的全部本地钩子均可以被仓库管理员替换或删除,这彻底取决于每一个团队成员是否实际使用钩子。咱们最好记住这一点,把 Git 钩子当作一个方便的开发者工具而非一个严格执行的开发政策。

即使如此,咱们也能够用服务端钩子来拒绝那些不符合标准的提交。咱们会在稍后讨论这一点。

本地钩子

本地钩子只影响它们所在的仓库。当咱们读到这一节时,要记住每位开发者均可以替换其本地的钩子,咱们不能使用钩子做为一种强制提交策略。然钩子却使得开发人员更容易坚持既定的开发方针。

在这一节里,咱们将展现6个最经常使用的本地钩子:

  • pre-commit
  • prepare-commit-msg
  • commit-msg
  • post-commit
  • post-checkout
  • pre-rebase

前四个钩子容许咱们在整个提交的生命周期中插入,后面两个容许咱们分别为 git checkout 和 git rebase 执行一些额外的行为或者安全检查。

全部的以 pre- 为前缀的钩子都容许咱们在后缀的行为 即将发生 的时候改变这些行为;而以 post- 为前缀的钩子 仅用于通知

咱们也会看到一些实用的技术来解析钩子参数,以及实用底层 Git 命令请求仓库信息。

预提交钩子 Pre-Commit

pre-commit 的脚本在每次运行 git commit 命令时,在 Git 要求开发者填写提交信息和生成提交对象以前执行。咱们能够用这个钩子在快照将要被提交的时刻对其进行检查。比方说,咱们也许想要在此时运行一些自动测试,以防止本次提交破坏现有功能。

pre-commit 脚本没有传入参数,而且以非零状态退出注1会终止整个提交。让咱们看一下一个简化的内置 pre-commit 钩子。这个脚本在发现有空白符的错误时会终止整个提交,空白符错误在 git diff-index 命令的 --check 参数里有详细定义(行尾空白符、仅有空白符的行、行首缩进的 tab 后面有空格等格式在默认状况下都会被认为是错误的)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/bin/sh

# 检测是否为初始提交
if git rev-parse --verify HEAD >/dev/null 2>&1
then
echo "pre-commit: About to create a new commit..."
against=HEAD
else
echo "pre-commit: About to create the first commit..."
against=4b825dc642cb6eb9a060e54bf8d69288fbee4904
fi

# 使用 git diff-index 来检测空白符错误
echo "pre-commit: Testing for whitespace errors..."
if ! git diff-index --check --cached $against
then
echo "pre-commit: Aborting commit due to whitespace errors"
exit 1
else
echo "pre-commit: No whitespace errors :)"
exit 0
fi

为了使用 git diff-index,咱们须要指出须要用来作比较的提交引用。这个引用一般来讲是 HEAD,可是在初始提交的时候不存在HEAD 指针,所以咱们的第一个任务就是处理这个边界值。咱们使用 git rev-parse --verify 命令,该命令能够检查参数是不是一个可用引用。>/dev/null 2>&1 的部分表示不显示全部 git rev-parse 命令的输出。将 HEAD 或是初始提交的空提交对象存放在against 变量中来给 git diff-index 使用,而 4b825d... 这个哈希值是一个魔术提交 ID,表示一个空提交。

git diff-index --cached 命令将当前暂存区与一个提交进行对比,传入 --check 选项表示咱们要求在改动包含空白符错误时给出警告。若是存在空白符错误,咱们终止提交并返回退出状态 1,不然咱们以 0 退出,且提交工做流正常工做。

这仅仅是 pre-commit 钩子的一个简单例子:使用 Git 命令来在由请求提交引入的更改上运行测试。咱们一样能够在 pre-commit 钩子中经过执行其余脚原本作任何想作的事情,例如运行一个第三方测试组件,或者使用 Lint 来检查代码格式。

准备提交信息钩子 Prepare Commit Message

prepare-commit-msg 钩子在 pre-commit 钩子以后调用,用于给提交的文本编辑器填充提交信息。这是一个替换压缩提交和合并提交时自动生成提交信息的一个好时机。

有 1 到 3 个参数会被传递给 prepare-commit-msg 脚本:

  1. 包含提交信息的临时文件名。咱们经过替换这个文件来更改提交信息。
  2. 提交的类型。包含 message (带有 -m 或 -F)、template (带有 -t)、merge(若是提交是一个合并提交)或者 squash(若是提交是从其余提交中合并的)。
  3. 相关提交的 SHA1 哈希值。只有带有 -c-C 或 --amend 的提交会给出这个参数。

和 pre-commit 同样,若是脚本以非零状态退出会停止提交。

在上一节咱们已经看到了一个简单的编辑提交信息的例子,可是仍是让咱们看一下一个更加有用的脚本。当使用 issue 来跟踪问题时,惯例是在一个单独的分支里解决一个对应的问题。若是咱们在分支名称上包含了 issue 号,咱们就能够编写一个 prepare-commit-msg 脚原本在这一分支的每个提交上都自动导入 issue 号。

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
#!/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 号,则生成带有 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 来获取当前分支。若是分支名字以 issue- 开头,脚本就会重写提交信息文件的内容,在开头的一行引入 issue 号。所以,若是你的分支名是 issue-224,运行脚本就会生成下列提交信息:

1
2
3
4
5
6
7
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

在使用 pre-commit-msg 钩子时,须要记住的一点是在使用者经过 -m 参数运行 git commit 命令时本钩子依然会被执行。这就意味着在使用 -m 输入提交信息的时候,上面的脚本会在提交信息里直接加入 ISSUE-[#] 的内容,用户也无法编辑它。咱们能够经过判断第二个参数(commit_type)是否等于 message 的方式来处理这个特殊状况。

而后,在不带 -m 参数的状况下,prepare-commit-msg 钩子也会容许用户编辑自动生成的信息,因此这更多的是一个提供便利的脚本而非强制执行的提交政策。若是想要强制性的政策,咱们须要在下一节讨论的 commit-msg 钩子。

提交信息钩子 Commit Message

commit-msg 钩子与 prepare-commit-msg 很类似,可是这个钩子在用户输入提交信息 以后 调用。这个钩子适宜用于警告开发者他们的提交信息不符合咱们团队的标准。

传递给这个钩子的惟一参数就是包含提交信息的临时文件名,若是钩子不喜欢用户输入的信息,它就会将改文件替换(与 prepare-commit-msg 的行为同样)或者以非零状态退出从而终止整个提交。

举例来讲,下面的脚本检查并确保用户不能删除上一节所述的 prepare-commit-msg 钩子自动生成的 issue 号 ISSUE-[#]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!/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

post-commit 钩子在 commit-msg 钩子以后当即出发。其能够改变 git commit 命令的输出,所以主要被用做通知提醒。

本脚本没有参数传入,且它的退出状态也不会影响到提交。对于大多数 post-commit 钩子来讲,咱们都但愿可以访问刚刚建立的那个提交。咱们可使用 git rev-parse HEAD 来获取最新提交的 SHA1 哈希值,或者使用 git log -l 来获取它的全部信息。

举个例子,若是咱们想要在每次提交快照时给咱们的领导发个邮件(但若是这样作可能会被领导揍一顿……),就能够添加下面的 post-commit 钩子:

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
#!/usr/bin/env python

import smtplib
from email.mime.text import MIMEText
from subprocess import check_output

# Get the git log --stat entry of the new commit
log = check_output(['git', 'log', '-1', '--stat', 'HEAD'])

# Create a plaintext email message
msg = MIMEText("Look, I'm actually doing some work:\n\n%s" % log)

msg['Subject'] = 'Git post-commit hook notification'
msg['From'] = 'mary@example.com'
msg['To'] = 'boss@example.com'

# Send the message
SMTP_SERVER = 'smtp.example.com'
SMTP_PORT = 587

session = smtplib.SMTP(SMTP_SERVER, SMTP_PORT)
session.ehlo()
session.starttls()
session.ehlo()
session.login(msg['From'], 'secretPassword')

session.sendmail(msg['From'], msg['To'], msg.as_string())
session.quit()

可使用 post-commit 来触发一个本地 持续集成系统 注2,可是大多数时间咱们会把这件事放在 post-receive 钩子里。这个钩子跑在服务端而非本地,且 每当 有开发者提交代码的时候都会运行,所以这里更加适合部署咱们的持续集成系统。

切换后钩子 Post-Checkout

post-checkout 钩子工做机制与 post-commit 钩子很像,其触发时机是每次咱们使用 git checkout 切换到一个新引用上。它大可用在清理你工做目录生成的文件,尽管这么用可能会让人困惑。

这个钩子接受三个参数,它的退出状态不会影响到 git checkout 命令。

  1. 以前 HEAD 的引用
  2. 新 HEAD 的引用
  3. 一个标志,告诉咱们切换的是分支仍是文件,分支是 1,文件是 0。

一般 Python 开发者们都会遇到一个蛋疼的问题:在切换分支的时候,以前生成的各类 .pyc 文件依然留在工做区内。解释器有时使用.pyc 而非 .py 做为源文件。为了不混乱,咱们可使用下面的钩子在每次切换新分支的时候清理全部的 .pyc 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/usr/bin/env python

import sys, os, re
from subprocess import check_output

# Collect the parameters
previous_head = sys.argv[1]
new_head = sys.argv[2]
is_branch_checkout = sys.argv[3]

if is_branch_checkout == "0":
print "post-checkout: This is a file checkout. Nothing to do."
sys.exit(0)

print "post-checkout: Deleting all '.pyc' files in working directory"
for root, dirs, files in os.walk('.'):
for filename in files:
ext = os.path.splitext(filename)[1]
if ext == '.pyc':
os.unlink(os.path.join(root, filename))

对于钩子脚原本说,当前工做目录永远被设定为仓库根目录,所以 os.walk('.') 能够遍历仓库中的每个文件。而后咱们就能够删掉哪些后缀名是 .pyc 的文件啦。

咱们也可使用 post-checkout 钩子基于已经切换到的分支来改变工做目录。例如咱们可能使用一个叫作 plugins 的分支来存储核心代码以外的各类插件。若是这些插件体积不小,且其余的分支并不须要,咱们就能够仅在切换到 plugins 分支时才有选择的进行插件构建。

预衍合钩子 Pre-Rebase

pre-rebase 钩子在使用 git rebase 命令作任何改变以前触发,本钩子能够很好的确保一些糟糕状况的发生。

钩子有两个参数:分岔起点的上游分支和正在被衍合的分支。当衍合当前分支时,第二个参数为空。脚本以非零状态退出会终止衍合。

例如,若是咱们在项目中彻底不容许衍合,可使用下面的钩子:

1
2
3
4
5
#!/bin/sh

# Disallow all rebasing
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 脚本。这个脚本更加睿智一点,会在特定的状况下拒绝衍合。它会严查你准备衍合的分支是否已经合并到主线分支上。若是已合并,再进行衍合就会产生混乱,所以脚本就会拒绝衍合。

服务端钩子

服务端钩子和本地钩子的工做机制相同,区别仅在于其部署在服务端仓库(例如中央仓库或开发者的公共仓库)。因为部署在这种官方仓库,一些服务端钩子能够做为一种强制政策来拒绝一些特定提交。

下文中咱们将讨论三种服务端钩子:

  • pre-receive
  • update
  • post-receive

全部的这些钩子都是为了让咱们可以在 Git 推送的不一样阶段作出反应。

服务端钩子的输出会显示在客户端控制台上,所以将信息返回给开发者十分容易。可是咱们应该记住这些脚本在结束执行以前不会返回终端的控制权,所以咱们应该慎重处理那些运行时间较长的操做。

预接收钩子 Pre-Receive

pre-receive 钩子在每次有用户执行 git push 来推送提交到仓库中时都会被执行。其仅能存在于那些做为推送目标的 远端仓库,而非在原始仓库。

钩子在每次引用被更新以前都会运行,所以适合按照咱们的意愿作成强制执行的开发政策。若是咱们不但愿某人进行推送,或者不喜欢某些提交信息的格式和提交内容,咱们均可以使用这个钩子拒绝推送。尽管咱们不能阻止开发者搞出一些乱七八糟的提交,但至少能使用 pre-receive 阻挡这些难看的提交进入中央仓库。

本脚本不传入参数,可是每一个正在被推送的引用都经过标准输入的方式分行传入脚本,格式以下:

<old-value> <new-value> <ref-name>

咱们能够经过最基本的 pre-receive 脚原本看到本钩子是怎么工做的,下面的脚本仅仅是读入了引用内容并进行了输出:

1
2
3
4
5
6
7
8
9
10
11
#!/usr/bin/env python

import sys
import fileinput

# Read in each ref that the user is trying to update
for line in fileinput.input():
print "pre-receive: Trying to push ref: %s" % line

# Abort the push
# sys.exit(1)

不过,这里仍是跟其余的钩子略有不一样,由于信息是经过标准输入而非命令行参数传入的。当将本钩子防止到远端仓库的 .git/hooks 中,并对 master 分支进行推送时,咱们就会看到以下信息出如今控制台上:

b6b36c697eb2d24302f89aa22d9170dfe609855b 85baa88c22b52ddd24d71f05db31f4e46d579095 refs/heads/master

咱们可使用这些 SHA1 哈希值和一些底层 Git 命令,来检查即将生成的改动,一般有如下的用法:

  • 拒绝包含衍合了上游分支的提交;
  • 阻止非快进合并;
  • 检查用户是否有权限来进行某些修改(多用于中央化的 Git 工做流)

若是多个引用被提交,返回非零状态会取消所有的引用。若是咱们想按照具体提交来接受或拒绝分支,就须要使用 update 钩子。

更新钩子 Update

update 钩子在 pre-receive 以后被调用,工做原理差很少。其也会在全部东西被真实提交以前执行,但却分别为每个推送的引用而调用。意思是说若是用户尝试推送四个分支,update 钩子就会尝试执行四次。与 pre-receive 不一样的是,该钩子不须要读入标准输入,想法其接受以下的三个参数:

  • 正在更新的引用名称;
  • 分支引用中存储的旧提交对象;
  • 分支引用中存储的新提交对象。

这与传递给 pre-receive 钩子的信息相同,可是因为 update 在每个分支更新时都会被触发,咱们能够拒绝一部分分支而容许其余的分支提交。

1
2
3
4
5
6
7
8
9
10
11
12
#!/usr/bin/env python

import sys

branch = sys.argv[1]
old_commit = sys.argv[2]
new_commit = sys.argv[3]

print "Moving '%s' from %s to %s" % (branch, old_commit, new_commit)

# Abort pushing only this branch
# sys.exit(1)

上面的 update 钩子仅仅输出了分支名和新旧提交哈希值。当给远端的仓库推送多个分支时,咱们能够看到每一个分支都执行了一次 print语句。

接受后钩子 Post-Receive

post-receive 钩子在成功推送以后调用,适宜用做进行消息提示。对许多工做流来讲,本钩子比 post-commit 更适合触发通知,由于钩子放在公共服务器上改起来方便,分发给每个用户放在本地机器上改都无法改。在持续集成系统里,常用 post-receive 钩子给其余开发者发邮件。

本钩子没有参数,可是会从标准输入接入与 pre-receive 同样的输入参数。

总结

经过本文咱们学习了如何使用 Git 钩子改变内部行为以及项目中特定事件触发时进行通知。钩子就是放置在 .git/hooks 路径下的普通脚本,这使得他们易于安装和制定。

咱们也研究了一些最普通的本地及服务端钩子,这使得咱们能够在整个开发的生命周期中插入操做。咱们如今知道了如何在提交建立、推送过程的每一步中执行自定义操做。只须要懂一点脚本知识,咱们就能够对 Git 仓库作不少想作的事情。