常见的Git钩子用法包括:鼓励某种提交办法,根据仓库的状态改变项目环境,以及完善持续集成事情流等。但是既然脚本是可定制化的,那么也就意味着可以利用Git钩子来自动化或者优化开拓流程中的任一方面。
在本章中我们会先从观点提及。然后我们会稽核几个在本地和做事端最常用的钩子。
所有的Git钩子都是Git在某个特定事宜发生时会实行的脚本。以是实在非常随意马虎安装并配置。

钩子可以配置在本地或者做事端仓库,他们也只会在某些特定动作实行时被触发。我们会在后面的部分谈论钩子的种别。本节的内容可以运用于本地或者做事端。
安装钩子钩子脚本文件常日放置于项目目录的.git/hooks文件夹下。Git会在初始化项目时自动在这个文件夹下放置一些样例脚本。如果你查看.git/hooks文件夹下,会找到如下的文件:
applypatch-msg.sample pre-push.samplecommit-msg.sample pre-rebase.samplepost-update.sample prepare-commit-msg.samplepre-applypatch.sample update.samplepre-commit.sample
这些文件基本上涵盖了可以利用的钩子,只不过.sample扩展名不会让脚本内容生效。安装一个钩子最大略的办法便是删除.sample扩展名。或者如果你从头开始写好了一个钩子脚本,只须要将其命名为上面所列的文件名并去除.sample扩展名。
举例来说,安装一个最大略的prepare-commit-msg钩子脚本。删除.sample扩展名,然后添加如下内容:
#!/bin/shecho "# Please include a useful commit message!" > $1
钩子脚本须要可以被实行,以是如果你是从头新建的脚本,可能须要改变文件权限。比如为了确保prepare-commit-msg脚本能够被实行,你须要实行下面你的命令:
chmod +x prepare-commit-msg
经由设置之后你会创造每次实行git commit命令时,上面定制化过的提交信息会作为默认提交信息。我们接下来会在Prepare Commit Message的段落详细核阅这是如何事情的。至于现在只须要明白我们是可以通过钩子脚本来定制化Git的某些内部功能。
内置的样例脚本的内容有关于各种钩子可以通报的参数文档,以是可以作为创建新脚本的参考。
脚本措辞内置的脚本措辞基本上是shell或者perl脚本,但是实际上你可以利用任何能够作为可实行脚本运行的措辞。脚本文件的第一行(#!/bin/sh)定义了该当利用哪种脚本阐明器。以是要利用其他的措辞,只须要将第一行改为新的实行器的路径即可。
举个例子,我们通过修正脚本阐明器路径,让prepare-commit-msg文件实行Python脚本而不是shell命令。
#!/usr/bin/env pythonimport sys, oscommit_msg_filepath = sys.argv[1]with open(commit_msg_filepath, 'w') as f: f.write("# Please include a useful commit message!")
把稳第一行被修正为Python脚本的阐明器。以及我们没有利用$1(shell办法)来获取第一个参数的引用,而是利用了sys.argv[1](python办法)(下面会详细阐明这个地方)
Git供应的这个能力非常强大,这将许可你在创建钩子的时候利用任何你习气的脚本措辞。
钩子的范围在指定Git仓库中,钩子都是存在于本地的,它们不会跟随git clone命令被复制到新的仓库中去,以是任何对付当前仓库有权限的人都可以对其进行修正。
这个特性对付为开拓团队配置钩子产生了深远的影响。首先,你须要找到一种办法让钩子脚本们在团队成员之间保持同步。其次,你没法逼迫开拓者按照指定办法创建提交——只能鼓励他们这么做。
为全体开拓团队掩护钩子有点棘手,由于.git/hooks目录不会像项目的其他部分一样被git clone下去,也不在Git的版本管理范畴内。一种大略的办理方案是把钩子文件件放在实际项目目录中(也便是放在.git目录之外)。这样做可以担保他们的行为与其他被版本管理系统管理的文件一样。在这种情形下安装钩子可以对.git/hooks路径创建连接符号(symlink)或者便是大略拷贝黏贴到.git/hooks路径下。
此外,Git也供应了一种称为模板目录Template Directory的机制可以自动安装钩子。在模板目录下的所有除了以.开头的文件,在利用git init或者git clone命令时都会被自动复制到.git目录下。
下面一小节先容确当地钩子都可以被仓库的所有者改变——乃至完备卸载。这完备取决于团队成员自己是否利用那个钩子。基于此,最好是把Git钩子当做方便开拓者自己的事情,而不是严格的开拓规范来利用。
与之相对应的,我们反而可以利用做事真个Git钩子来谢毫不符合规范的提交。关于这一点我们会在本文的稍后部分进行谈论。
本地钩子本地钩子只会影响本地仓库。既然你已经读到这里,想必一定会记得每个开拓者自己可以修合法地钩子,以是无法将其作为一种提交规范逼迫实行。不过它们可以让开发者能够更方便地遵照某种辅导方针。
在本小节,我们会先容6中最常用确当地钩子:
pre-commitprepare-commit-msgcommit-msgpost-commitpost-checkoutpre-rebase前四个可以用于提交的完全生命周期,后两个用于实行在git checkout和git rebase之后的安全检讨。
所有pre-开头的钩子都是在实际动作实行前会被触发,post-开头的则是在实际动作实行之后被触发。
接下来我们还会须要利用一些底层的Git命令来解析钩子参数或者查询仓库信息。
Pre-Commit每一次实行git commit命令时,在哀求填入提交信息或者天生提交工具之前,pre-commit脚本会被触发实行。可以利用这个钩子检讨即将要提交的仓库快照。比如说你可能会想在这个时点实行一些测试,以担保新的提交不会毁坏已有的功能。
pre-commit脚本不须要传入参数,脚本实行退出旗子暗记不为0时(non-zero signal)会终止全体提交。下面我们看看内置pre-commit钩子的大略版本(而且可交互)。如果在提交中找到空缺缺点时会退出提交,空缺缺点利用git diff-index命令进行查找(尾随空格——包括单独由空格组成的行——和空格字符——紧跟该行的初始缩进内的制表符后面的空格字符——将被视为空缺缺点)。
#!/bin/sh# Check if this is the initial commitif git rev-parse --verify HEAD >/dev/null 2>&1then echo "pre-commit: About to create a new commit..." against=HEADelse echo "pre-commit: About to create the first commit..." against=4b825dc642cb6eb9a060e54bf8d69288fbee4904fi# Use git diff-index to check for whitespace errorsecho "pre-commit: Testing for whitespace errors..."if ! git diff-index --check --cached $againstthen echo "pre-commit: Aborting commit due to whitespace errors" exit 1else echo "pre-commit: No whitespace errors :)" exit 0fi
为了能够利用git diff-index,我们须要确定用于比较的提交。常日来说是利用HEAD;然而在初试提交时并不存在HEAD,我们首先要考虑这一边缘用例。我们利用 git rev-parse --verify来进行参数(HEAD)的大略校验。>/dev/null 2>&1那行会静默输出git rev-parse的内容。不管是HEAD还是空提交工具都会被存储在against变量中,在后边用于git diff-index的参数。哈希字符4b825d...是用来表示空提交的邪术字符。
git diff-index会比较提交与索引。通过传入--check选项会让其检讨到这次提交引入了空缺缺点时发出一个警告。如果命令返回了警告,脚本会返回退出状态为1以便退出提交,否则会返回退出状态0以便提互换程连续。
这仅是pre-commit钩子的一种示例。我们只是适值利用这个例子来对即将提交的代码进行一些大略的测试,你仍旧可以利用pre-commit来做任何想做的事情,乃至引用其他脚本,或者实行第三方的测试套件,或者利用Lint检讨代码风格。
Prepare Commit Message实行完成pre-commit钩子脚本之后会触发prepare-commit-msg钩子,它会弹出含有提交信息的文本编辑器。在这一步可以用来修正squash或者merge命令自动天生的提交信息。
prepare-commit-msg脚本接管的三个参数如下:
储存提交信息的临时文件名称。你可以直接修正这个文件的内容来改变提交信息。提交类型。可以是message(-m或者-F选项),template(-t选项),merge(如果本次提交时合并提交),或者squash(如果提交squash了其他提交)关联提交的SHA1哈希值。仅当利用-c, -C, --amend选项时可传。与pre-commit一样,当退出状态非0时退出提交。
我们之前已经见过一个大略的用于编辑提交信息的例子,现在我们来看一下更有利用代价的脚本。如果开拓团队利用问题跟踪软件来管理需求和毛病,比如Jira, BugZilla,Redmine等,常日老例是为每一个issue指定一个独立分支。如果团队规约中规定分支名须要包含issueid,你可以定制prepare-commit-msg自动把issue id填写到提交信息中。
#!/usr/bin/env pythonimport sys, os, refrom subprocess import check_output# Collect the parameterscommit_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)# Figure out which branch we're onbranch = check_output(['git', 'symbolic-ref', '--short', 'HEAD']).strip()print "prepare-commit-msg: On branch '%s'" % branch# Populate the commit message with the issue #, if there is oneif 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 number添加到提交信息的第一行。比如说你的分支名称为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选项实行git commit命令并传入了提交信息,prepare-commit-msg也仍旧会实行。也便是说上面的脚本会自动插入ISSUE-[#]之类的字符串而不是事先给用户编辑的机会。你可以通过判断传入的第二个参数(commit_type)是否为message来加以处理。
不过没有传入-m选项时,prepare-commit-msg钩子倒是许可用户在天生提交信息之后再来编辑它。以是这个钩子确实只是一个方便天生提交信息的脚本,而不太适宜作为逼迫提交信息规范。至于这一点,你可能更须要的是下一小节会谈论的commit-msg钩子。
Commit Messagecommit-msg与prepare-commit-msg很像,但它是在用户输入了提交信息之后触发实行的。如果须要警告开拓者的提交信息不符合团队规范,此时是一个得当的机遇。
可以通报的唯一参数是存储提交信息的文件名。如果不喜好用户输入的提交信息,可以在此机遇自动对其进行修正,或者直接中断提互换程。
如下例中,脚本会检讨用户是否删除了prepare-commit-msg钩子自动添加的ISSUE-[]字符串:
#!/usr/bin/env pythonimport sys, os, refrom subprocess import check_output# Collect the parameterscommit_msg_filepath = sys.argv[1]# Figure out which branch we're onbranch = check_output(['git', 'symbolic-ref', '--short', 'HEAD']).strip()print "commit-msg: On branch '%s'" % branch# Check the commit message if we're on an issue branchif 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-Commitpost-commit钩子总会在commit-msg钩子之后立即实行。它不能修正git commit操作本身,以是紧张用于关照。
该脚本不须要传入参数,而且其退出状态码不会影响提交结果。对付大多数post-commit脚本,会须要操作刚刚创建的提交本身。你可以通过git rev-parse HEAD命令来获取最近这次提交的SHA1哈希值,或者利用git log -1 HEAD获取最近这次提交的所有信息。
比如如果在每一次提交之后你想通过email关照你的老板(可能不是什么好主张),你可以将如下脚本添加到post-commit钩子中:
#!/usr/bin/env pythonimport smtplibfrom email.mime.text import MIMETextfrom subprocess import check_output# Get the git log --stat entry of the new commitlog = check_output(['git', 'log', '-1', '--stat', 'HEAD'])# Create a plaintext email messagemsg = 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 messageSMTP_SERVER = 'smtp.example.com'SMTP_PORT = 587session = 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中触发一次持续集成,不过大多数情形下这一流程是通过post-receive钩子触发的。这个钩子在做事器上实行而不是本地机器。每一次远程做事器收到开拓者推送的代码都会触发这个钩子。因此这个钩子更加适宜实行持续及集成的任务。
Post-Checkoutpost-checkout的事情模式与post-commit很像,差异在于每次实行git checkout命令成功检出一个分支或者提交时触发。这对付清理那些会产生混乱的自动天生文件是一个好机遇。
它就接管三个参数,而且退出状态码对付git checkout命令结果不产生影响。
当前的HEAD引用新的HEAD引用一个用于区分本次checkout是针对分支的还是针对文件的。选项值分别为1和0。一个Python开拓者常见的场景是,天生的.pyc文件会跟随分支切换。而阐明器有时候会优先利用.pyc文件而不是从.py文件开始编译。因此在切换分支时常常会造成困惑,应对办法便是在每次切换分支时利用post-checkout脚本删除所有.pyc文件:
#!/usr/bin/env pythonimport sys, os, refrom subprocess import check_output# Collect the parametersprevious_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))
对付钩子脚本来说,当前事情目录总是被定位于Git项目的根目录,因此os.walk('.')命令会递归地查找全体仓库文件夹,找到.pyc文件并删除它。
你也可以利用post-checkout钩子根据要检出的分支对事情目录进行修正。比如当你利用plugins分支来存储核心代码仓库以外的插件时。假设这些插件依赖许多其他分支不须要的二进制文件,你可以选择性的在切换到plugins分支时才去编译构建。
Pre-Rebasepre-rebase钩子在git rebase实行之前被触发,在此机遇可以进行检讨以避免发生毁坏性的事情。
该钩子接管两个参数:上游分支,和进行rebase的分支。当rebase的分支为当前分支时,第二个参数为空。钩子脚本退出状态码非0时,退出rebase。
举例来说假设你在仓库中禁止任何rebase操作,可以利用如下的pre-rebase脚本:
#!/bin/sh# Disallow all rebasingecho "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的分支是否已经被合并入next分支(即假设的主分支)。如果已经被合并过,那么很可能会产生问题,以是脚本会中断这次rebase操作。
做事端钩子做事真个钩子与本地钩子类似,只是他们存在于做事端仓库(比如一个中央仓库,或者开拓者的共有仓库)。当作为中央节点仓库利用时,这些钩子可以通过谢绝某些提交来逼迫实行提交规范。
我们会在接下来的篇幅中谈论如下三个做事端钩子
pre-receiveupdatepost-receive以上三种钩子用于处理git push进程的不同阶段。
做事端钩子的输出信息会返回给客户端掌握台,以是给开拓者返复书息非常方便。不过要把稳的是这些脚本在实行完毕之前不会将终端掌握权交还给开拓者,以是请避免在这些钩子中实行过于耗时的操作。
Pre-Receive每当有用户利用git push命令推送提交到仓库时,pre-receive钩子就会被触发。这个钩子脚本该当放置于远程仓库,用于吸收推送,而不是发起推送的仓库。
此钩子会在更新仓库的提交引用之前被实行,因此非常适宜用于逼迫开拓规范。对付诸如谁不能实行推送到什么分支,提交信息格式不合规范,或者提交中含有特定禁止的内容时,可以通过该脚本对其进行谢绝操作。虽然我们不能阻挡开拓者在本地进行不合规的提交,但是我们总是可以利用pre-receive钩子来谢绝这些不合规的提交进入中央仓库。
该脚本不接管参数,但是推送的引用会按照下面的格式通报给脚本:
<old-value> <new-value> <ref-name>
参照下面的大略例子可以看到pre-receive脚本是如何读取推送的引用并且打印出来:
#!/usr/bin/env pythonimport sysimport fileinput# Read in each ref that the user is trying to updatefor line in fileinput.input(): print "pre-receive: Trying to push ref: %s" % line# Abort the push# sys.exit(1)
可以看出该钩子与其他钩子有一些细微的差别,通报给脚本的信息是通过标准输入而不是通过命令行参数。将脚本放置于远程仓库的.git/hooks目录下之后,推送main分支的操作会返回如下的输出:
b6b36c697eb2d24302f89aa22d9170dfe609855b 85baa88c22b52ddd24d71f05db31f4e46d579095 refs/heads/main
进阶利用pre-receive钩子时可以利用这些SHA1哈希值,合营一些底层的Git命令,可以用来检讨即将被引入的代码修正。一些常见的用例包括:
谢绝涉及rebase上游分支的变更阻挡非快速提高的合并检讨用户是否拥有足够的权限(常日用于中央化的Git事情流)如果多个引用同时被推送,返回非0的退出状态码会中断所有推送。如果你想一个一个的判断是否接管或者谢绝,可以利用update钩子。
Updateupdate钩子会在pre-receive之后被触发实行,它们的事情模式也基本类似。该钩子仍旧是在发生任何改变之前被实行,但它是根据推送的多个引用分别被调用。也便是说如果用户考试测验推送4个分支,update会实行4次。与pre-receive不同,该钩子不须要从标准输入读取信息,而是接管下面三个参数:
要更新的引用名称存储在引用中的旧的提交工具名称存储在引用中的新的提交工具名称这与pre-receive钩子接管的信息一样,但是由于update是根据需更新的引用分别实行,因此你可以在通过一些更新的同时谢绝其余一些。
#!/usr/bin/env pythonimport sysbranch = 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-Receivepost-receive钩子在成功推送操作之后被触发,因此适宜用于发送关照。对付很多事情流来说,此时触发关照比post-commit触发关照更得当,由于此时此刻变更已经存在于公共做事器上,而不仅仅存在于用户确当地机器。向其他开拓者发送邮件或者触发持续集成之类的操作是post-receive钩子的常见用例。
该脚本不接管参数,但与pre-receive一样从标准输入获取同样的信息。
总结在本文中我们学习了Git钩子如何被运用与改变内部行为,以及在仓库中特定事宜发生时如何收到关照。钩子也是普通脚本,他们都放置于仓库目录的.git/hooks目录下,因此易于安装和定制化。
我们也理解了一些常用确当地和做事端钩子。这些钩子许可我们将流程插入到完全的开拓周期中。现在我们已经理解如何在创建提交的不同阶段实行可定制的操作,以及同样的定制化操作如何在git push过程中实行。具备一些脚本编程能力,可以让你有能力在Git仓库的任何流程中做任何想做的事情。