Revise 5.4 Git钩子:自定义你的工作流.

This commit is contained in:
ZhongyiTong
2015-12-10 00:56:37 +08:00
parent c1529ad3ba
commit 53f024341c

View File

@ -4,19 +4,19 @@
> >
> 这是一篇在[原文(BY atlassian)](https://www.atlassian.com/git/tutorials/git-hooks)基础上演绎的译文。除非另行注明,页面上所有内容采用知识共享-署名([CC BY 2.5 AU](http://creativecommons.org/licenses/by/2.5/au/deed.zh))协议共享。 > 这是一篇在[原文(BY atlassian)](https://www.atlassian.com/git/tutorials/git-hooks)基础上演绎的译文。除非另行注明,页面上所有内容采用知识共享-署名([CC BY 2.5 AU](http://creativecommons.org/licenses/by/2.5/au/deed.zh))协议共享。
Git钩子是在Git仓库中特定事件发生时自动运行的脚本。它可以让你自定义Git内部的行为在开周期中的关键点触发自定义的行为。 Git钩子是在Git仓库中特定事件发生时自动运行的脚本。它可以让你自定义Git内部的行为在开周期中的关键点触发自定义的行为。
![enter image description here](https://www.atlassian.com/git/images/tutorials/advanced/git-hooks/01.svg) ![enter image description here](https://www.atlassian.com/git/images/tutorials/advanced/git-hooks/01.svg)
Git钩子最常见的使用场景包括推行提交规范根据仓库状态改变项目环境和接入持续集成工作流。但是因为脚本可以完全定制你可以用Git钩子来自动化或者优化你开发工作流中任意部分。 Git钩子最常见的使用场景包括推行提交规范根据仓库状态改变项目环境和接入持续集成工作流。但是因为脚本可以完全定制你可以用Git钩子来自动化或者优化你开发工作流中任意部分。
在这篇文章中我们会先简要介绍Git钩子是如何工作的。然后我们会审视一些本地和远端仓库使用最流行的钩子。 在这篇文章中我们会先简要介绍Git钩子是如何工作的。然后我们会审视一些本地和远端仓库使用最流行的钩子。
# 概述 # 概述
所有Git钩子是仓库中特定事件发生时Git自动运行的普通脚本。因此Git钩子安装和配置也非常容易。 Git钩子是仓库中特定事件发生时Git自动运行的普通脚本。因此Git钩子安装和配置也非常容易。
钩子在本地或服务端仓库都可以部署,而且最会在仓库中事件发生时被执行。在文章后面我们会具体地研究各种钩子。接下来所讲的配置对本地和服务端钩子都起作用。 钩子在本地或服务端仓库都可以部署,且只会在仓库中事件发生时被执行。在文章后面我们会具体地研究各种钩子。接下来所讲的配置对本地和服务端钩子都起作用。
### 安装钩子 ### 安装钩子
@ -46,7 +46,7 @@ echo "# Please include a useful commit message!" > $1
chmod +x prepare-commit-msg chmod +x prepare-commit-msg
``` ```
接下来你每次运行`git commit` 你会看到默认的提交信息都被替换了。我们会在“准备提交信息”一节中细看它是如何工作的。现在我们已经可以定制Git的内部功能你只要开心就好 接下来你每次运行`git commit`时你会看到默认的提交信息都被替换了。我们会在“准备提交信息”一节中细看它是如何工作的。现在我们已经可以定制Git的内部功能你只需要坐和放宽
内置的样例脚本是非常有用的参考资料,因为每个钩子传入的参数都有非常详细的说明(不同钩子不一样)。 内置的样例脚本是非常有用的参考资料,因为每个钩子传入的参数都有非常详细的说明(不同钩子不一样)。
@ -54,8 +54,6 @@ chmod +x prepare-commit-msg
内置的脚本大多是shell和PERL语言的但你可以使用任何脚本语言只要它们最后能编译到可执行文件。每次脚本中的`#!/bin/sh`定义了你的文件将被如何解释。比如使用其他语言时你只需要将path改为你的解释器的路径。 内置的脚本大多是shell和PERL语言的但你可以使用任何脚本语言只要它们最后能编译到可执行文件。每次脚本中的`#!/bin/sh`定义了你的文件将被如何解释。比如使用其他语言时你只需要将path改为你的解释器的路径。
比如说,你可以在`prepare-commit-msg`中写一个可执行的Python脚本。下面这个钩子和上一节的shell脚本做的事完全一样。 比如说,你可以在`prepare-commit-msg`中写一个可执行的Python脚本。下面这个钩子和上一节的shell脚本做的事完全一样。
``` ```
@ -84,7 +82,7 @@ with open(commit_msg_filepath, 'w') as f:
作为备选方案Git同样提供了一个模板目录机制来更简单地自动安装钩子。每次你使用`git init` 或`git clone`时,模板目录文件夹下的所有文件和目录都会被复制到`.git` 文件夹。 作为备选方案Git同样提供了一个模板目录机制来更简单地自动安装钩子。每次你使用`git init` 或`git clone`时,模板目录文件夹下的所有文件和目录都会被复制到`.git` 文件夹。
所有的下面讲到的本地钩子都可以被更改或者彻底删除,只要你是项目的所有者。这完全取决于你的团队成员想不想用这个钩子。所以记住最好把Git钩子当成一个方便的开发者工具而不是一个严格强制的开发规范。 所有的下面讲到的本地钩子都可以被更改或者彻底删除,只要你是项目的参与者。这完全取决于你的团队成员想不想用这个钩子。所以记住最好把Git钩子当成一个方便的开发者工具而不是一个严格强制的开发规范。
也就是说,用服务端钩子来拒绝没有遵守规范的提交是完全可行的。后面我们会再讨论这个问题。 也就是说,用服务端钩子来拒绝没有遵守规范的提交是完全可行的。后面我们会再讨论这个问题。
@ -101,7 +99,7 @@ with open(commit_msg_filepath, 'w') as f:
- post-checkout - post-checkout
- pre-rebase - pre-rebase
4个钩子让你介入完整的提交生命周期,后2个允许你执行一些额外的操作,分别为`git checkout` 和`git rebase` 的安全检查。 个钩子让你介入完整的提交生命周期,后个允许你执行一些额外的操作,分别为`git checkout`和`git rebase`的安全检查。
所有带`pre-` 的钩子允许你修改即将发生的操作,而带`post-` 的钩子只能用于通知。 所有带`pre-` 的钩子允许你修改即将发生的操作,而带`post-` 的钩子只能用于通知。
@ -116,7 +114,7 @@ with open(commit_msg_filepath, 'w') as f:
``` ```
#!/bin/sh #!/bin/sh
# Check if this is the initial commit # 检查这是否是初始提交
if git rev-parse --verify HEAD >/dev/null 2>&1 if git rev-parse --verify HEAD >/dev/null 2>&1
then then
echo "pre-commit: About to create a new commit..." echo "pre-commit: About to create a new commit..."
@ -126,7 +124,7 @@ else
against=4b825dc642cb6eb9a060e54bf8d69288fbee4904 against=4b825dc642cb6eb9a060e54bf8d69288fbee4904
fi fi
# Use git diff-index to check for whitespace errors # 使用git diff-index来检查空白字符错误
echo "pre-commit: Testing for whitespace errors..." echo "pre-commit: Testing for whitespace errors..."
if ! git diff-index --check --cached $against if ! git diff-index --check --cached $against
then then
@ -138,7 +136,7 @@ else
fi fi
``` ```
为了使用`git diff-index` 我们要指出我们正在和那次提交比较。一般来说是HEAD但HEAD在创建第一次提交时不存在所以我们的第一个任务是解决这个极端情形。我们通过`git rev-parse --verify` 来检查HEAD是否是一个合法的引用。`>/dev/null 2>&1` 这部分屏蔽了`git rev-parse` 任何输出。HEAD或者一个新的提交对象被储存在`against` 变量中供`git diff-index` 使用。`4b825d...` 这个哈希值是代表一个空白提交的ID。 使用`git diff-index`我们要指出和哪次提交进行比较。一般来说是HEAD但HEAD在创建第一次提交时不存在所以我们的第一个任务是解决这个极端情形。我们通过`git rev-parse --verify`来检查HEAD是否是一个合法的引用。`>/dev/null 2>&1`这部分屏蔽了`git rev-parse`任何输出。HEAD或者一个新的提交对象被储存在`against`变量中供`git diff-index`使用。`4b825d...`这个哈希字串代表一个空白提交的ID。
`git diff-index --cached`命令将提交和缓存区比较。通过传入`-check`选项我们要求它在更改引入空白字符错误时警告我们。如果它这么做了我们返回状态1来放弃这次提交否则返回状态0提交工作流正常进行。 `git diff-index --cached`命令将提交和缓存区比较。通过传入`-check`选项我们要求它在更改引入空白字符错误时警告我们。如果它这么做了我们返回状态1来放弃这次提交否则返回状态0提交工作流正常进行。
@ -153,8 +151,8 @@ fi
`prepare-commit-msg`脚本的参数可以是下列三个: `prepare-commit-msg`脚本的参数可以是下列三个:
- 包含提交信息的文件名。你可以在原地更改提交信息。 - 包含提交信息的文件名。你可以在原地更改提交信息。
- 提交类型。可以是信息(`-m``-F` 选项),模板(`-t` 选项merge如果是个merge提交或squash如果这个提交插入了其他提交 - 提交类型。可以是信息(`-m``-F`选项),模板(`-t`选项merge如果是个合并提交或squash如果这个提交插入了其他提交
- 相关提交的SHA1哈希。只有当`-c`, `-C`, or `--amend` 选项出现时才会出现 - 相关提交的SHA1哈希字串。只有当`-c``-C`,或`--amend`选项出现时才需要
和`pre-commit`一样以非0状态退出会放弃提交。 和`pre-commit`一样以非0状态退出会放弃提交。
@ -166,7 +164,7 @@ fi
import sys, os, re import sys, os, re
from subprocess import check_output from subprocess import check_output
# Collect the parameters # 收集参数
commit_msg_filepath = sys.argv[1] commit_msg_filepath = sys.argv[1]
if len(sys.argv) > 2: if len(sys.argv) > 2:
commit_type = sys.argv[2] commit_type = sys.argv[2]
@ -179,11 +177,11 @@ else:
print "prepare-commit-msg: File: %s\nType: %s\nHash: %s" % (commit_msg_filepath, commit_type, 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 on # 检测我们所在的分支
branch = check_output(['git', 'symbolic-ref', '--short', 'HEAD']).strip() branch = check_output(['git', 'symbolic-ref', '--short', 'HEAD']).strip()
print "prepare-commit-msg: On branch '%s'" % branch print "prepare-commit-msg: On branch '%s'" % branch
# Populate the commit message with the issue #, if there is one # 用issue编号生成提交信息
if branch.startswith('issue-'): if branch.startswith('issue-'):
print "prepare-commit-msg: Oh hey, it's an issue branch." print "prepare-commit-msg: Oh hey, it's an issue branch."
result = re.match('issue-(.*)', branch) result = re.match('issue-(.*)', branch)
@ -207,7 +205,7 @@ ISSUE-224
# modified: test.txt # modified: test.txt
``` ```
有一点要记住的是即使用户用`-m` 传入提交信息`prepare-commit-msg` 也会运行。也就是说,上面这个脚本会自动插入`ISSUE-[#]` 字符串,而用户无法更改。你可以检查第二个参数是否是提交类型来处理这个情况。 有一点要记住的是即使用户用`-m`传入提交信息,`prepare-commit-msg` 也会运行。也就是说,上面这个脚本会自动插入`ISSUE-[#]`字符串,而用户无法更改。你可以检查第二个参数是否是提交类型来处理这个情况。
但是,如果没有`-m`选项,`prepare-commit-msg`钩子允许用户修改生成后的提交信息。所以脚本的目的是为了方便,而不是推行强制的提交信息规范。如果你要这么做,你需要下一节所讲的`commit-msg`钩子。 但是,如果没有`-m`选项,`prepare-commit-msg`钩子允许用户修改生成后的提交信息。所以脚本的目的是为了方便,而不是推行强制的提交信息规范。如果你要这么做,你需要下一节所讲的`commit-msg`钩子。
@ -225,14 +223,14 @@ ISSUE-224
import sys, os, re import sys, os, re
from subprocess import check_output from subprocess import check_output
# Collect the parameters # 收集参数
commit_msg_filepath = sys.argv[1] commit_msg_filepath = sys.argv[1]
# Figure out which branch we're on # 检测所在的分支
branch = check_output(['git', 'symbolic-ref', '--short', 'HEAD']).strip() branch = check_output(['git', 'symbolic-ref', '--short', 'HEAD']).strip()
print "commit-msg: On branch '%s'" % branch print "commit-msg: On branch '%s'" % branch
# Check the commit message if we're on an issue branch # 检测提交信息判断是否是一个issue提交
if branch.startswith('issue-'): if branch.startswith('issue-'):
print "commit-msg: Oh hey, it's an issue branch." print "commit-msg: Oh hey, it's an issue branch."
result = re.match('issue-(.*)', branch) result = re.match('issue-(.*)', branch)
@ -250,11 +248,11 @@ if branch.startswith('issue-'):
### post-commit ### post-commit
`post-commit` 钩子在`commit-msg` 钩子之后立即被运行 。它不能更改`git commit` 的结果,所以这主要用于通知用途。 `post-commit`钩子在`commit-msg`钩子之后立即被运行 。它无法更改`git commit`的结果,所以这主要用于通知用途。
这个脚本没有参数,而且退出状态不会影响提交。对于大多数`post-commit` 脚本来说,你只是想访问你刚刚创建的提交。你可以用`git rev-parse HEAD` 来获得最近一次提交的SHA1哈希,或者你可以用`git log -l HEAD` 获取完整的信息。 这个脚本没有参数,而且退出状态不会影响提交。对于大多数`post-commit`脚本来说,你只是想访问你刚刚创建的提交。你可以用`git rev-parse HEAD`来获得最近一次提交的SHA1哈希字串,或者你可以用`git log -l HEAD`获取完整的信息。
比如说,如果你需要每次提交快照时向老板发封邮件(也许对于大多数工作流来不是个好的想法),你可以加上下面这个`post-commit` 钩子。 比如说,如果你需要每次提交快照时向老板发封邮件(也许对于大多数工作流来说这不是个好的想法),你可以加上下面这个`post-commit`钩子。
``` ```
#!/usr/bin/env python #!/usr/bin/env python
@ -263,17 +261,17 @@ import smtplib
from email.mime.text import MIMEText from email.mime.text import MIMEText
from subprocess import check_output from subprocess import check_output
# Get the git log --stat entry of the new commit # 获得新提交的git log --stat输出
log = check_output(['git', 'log', '-1', '--stat', 'HEAD']) 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 = MIMEText("Look, I'm actually doing some work:\n\n%s" % log)
msg['Subject'] = 'Git post-commit hook notification' msg['Subject'] = 'Git post-commit hook notification'
msg['From'] = 'mary@example.com' msg['From'] = 'mary@example.com'
msg['To'] = 'boss@example.com' msg['To'] = 'boss@example.com'
# Send the message # 发送信息
SMTP_SERVER = 'smtp.example.com' SMTP_SERVER = 'smtp.example.com'
SMTP_PORT = 587 SMTP_PORT = 587
@ -291,9 +289,7 @@ session.quit()
### post-checkout ### post-checkout
`post-checkout`钩子和`post-commit`钩子很像,但它在你用`git checkout`查看引用的时候被调用。这是用来清理你的工作目录中可能会令人困惑的生成文件。
`post-checkout` 钩子和`post-commit` 钩子很像,但它是在你用`git checkout` 查看引用的时候被调用的。这是用来清理你的工作目录中可能会令人困惑的生成文件。This is nice for clearing out your working directory of generated files that would otherwise cause confusion.
这个钩子接受三个参数,它的返回状态不影响`git checkout`命令。 这个钩子接受三个参数,它的返回状态不影响`git checkout`命令。
@ -309,7 +305,7 @@ Python程序员经常遇到的问题是切换分支后那些之前生成的`.pyc
import sys, os, re import sys, os, re
from subprocess import check_output from subprocess import check_output
# Collect the parameters # 收集参数
previous_head = sys.argv[1] previous_head = sys.argv[1]
new_head = sys.argv[2] new_head = sys.argv[2]
is_branch_checkout = sys.argv[3] is_branch_checkout = sys.argv[3]
@ -326,7 +322,7 @@ for root, dirs, files in os.walk('.'):
os.unlink(os.path.join(root, filename)) os.unlink(os.path.join(root, filename))
``` ```
钩子脚本当前的工作目录总是仓库的根目录下,所以`os.walk('.')` 调用遍历了仓库中所有文件。接下来,我们检查它的拓展名,如果是`.pyc` 就删除它。 钩子脚本当前的工作目录总是位于仓库的根目录下,所以`os.walk('.')`调用遍历了仓库中所有文件。接下来,我们检查它的拓展名,如果是`.pyc`就删除它。
通过`post-checkout`钩子你还可以根据你切换的分支来来更改工作目录。比如说你可以在代码库外面使用一个插件分支来储存你所有的插件。如果这些插件需要很多二进制文件而其他分支不需要你可以选择只在插件分支上build。 通过`post-checkout`钩子你还可以根据你切换的分支来来更改工作目录。比如说你可以在代码库外面使用一个插件分支来储存你所有的插件。如果这些插件需要很多二进制文件而其他分支不需要你可以选择只在插件分支上build。
@ -334,14 +330,14 @@ for root, dirs, files in os.walk('.'):
`pre-rebase`钩子在`git rebase`发生更改之前运行,确保不会有什么糟糕的事情发生。 `pre-rebase`钩子在`git rebase`发生更改之前运行,确保不会有什么糟糕的事情发生。
这个钩子有两个参数frok之前的上游分支将要rebase的下游分支。如果rebase当前分支的话第二个参数是空的。以非0状态退出会放弃这次rebase。 这个钩子有两个参数frok之前的上游分支将要rebase的下游分支。如果rebase当前分支第二个参数为空。以非0状态退出会放弃这次rebase。
比如说如果你想彻底禁用rebase操作你可以使用下面的`pre-rebase`脚本: 比如说如果你想彻底禁用rebase操作你可以使用下面的`pre-rebase`脚本:
``` ```
#!/bin/sh #!/bin/sh
# Disallow all rebasing # 禁用所有rebase
echo "pre-rebase: Rebasing is dangerous. Don't do it." echo "pre-rebase: Rebasing is dangerous. Don't do it."
exit 1 exit 1
``` ```
@ -353,7 +349,7 @@ pre-rebase: Rebasing is dangerous. Don't do it.
The pre-rebase hook refused to rebase. The pre-rebase hook refused to rebase.
``` ```
内置的`pre-rebase.sample` 脚本是一个更复杂的例子。它在什么时候阻止rebase这方面更加智能。它会检查你当前的分支是否已经合并到了下一个分支中去也就是主分支。如果是的话rebase可能会遇到问题脚本会放弃这次rebase。 内置的`pre-rebase.sample`脚本是一个更复杂的例子。它在何时阻止rebase这方面更加智能。它会检查你当前的分支是否已经合并到了下一个分支中去也就是主分支。如果是的话rebase可能会遇到问题脚本会放弃这次rebase。
# 服务端钩子 # 服务端钩子
@ -389,11 +385,11 @@ The pre-rebase hook refused to rebase.
import sys import sys
import fileinput import fileinput
# Read in each ref that the user is trying to update # 读取用户试图更新的所有引用
for line in fileinput.input(): for line in fileinput.input():
print "pre-receive: Trying to push ref: %s" % line print "pre-receive: Trying to push ref: %s" % line
# Abort the push # 放弃推送
# sys.exit(1) # sys.exit(1)
``` ```
@ -403,7 +399,7 @@ for line in fileinput.input():
b6b36c697eb2d24302f89aa22d9170dfe609855b 85baa88c22b52ddd24d71f05db31f4e46d579095 refs/heads/master b6b36c697eb2d24302f89aa22d9170dfe609855b 85baa88c22b52ddd24d71f05db31f4e46d579095 refs/heads/master
``` ```
你可以用SHA1哈希或者底层的Git命令来检查将要引入的更改。一些常见的使用包括 你可以用SHA1哈希字串或者底层的Git命令来检查将要引入的更改。一些常见的使用包括
- 拒绝将上游分支rebase的更改 - 拒绝将上游分支rebase的更改
- 防止错综复杂的合并(非快速向前,会造成项目历史非线性) - 防止错综复杂的合并(非快速向前,会造成项目历史非线性)
@ -431,15 +427,15 @@ new_commit = sys.argv[3]
print "Moving '%s' from %s to %s" % (branch, old_commit, new_commit) print "Moving '%s' from %s to %s" % (branch, old_commit, new_commit)
# Abort pushing only this branch # 只放弃当前分支的推送
# sys.exit(1) # sys.exit(1)
``` ```
上面这个钩子简单地输出了分支和新旧提交的哈希。当你向远程仓库推送超过一个分支时,你可以看到每个分支都有输出。 上面这个钩子简单地输出了分支和新旧提交的哈希字串。当你向远程仓库推送超过一个分支时,你可以看到每个分支都有输出。
### post-receive ### post-receive
`post-receive` 钩子在成功推送后被调用,适合用于发送通知。对很多工作流来说,这比`post-commit` 都是一个更好的发送通知的地方,因为这些更改在公共的服务器而不是用户的本地机器上。给其他开发者发送邮件或者触发一个持续集成系统都是`post-receive` 常用的操作。 `post-receive`钩子在成功推送后被调用,适合用于发送通知。对很多工作流来说,这是一个比`post-commit`更好的发送通知的地方,因为这些更改在公共的服务器而不是用户的本地机器上。给其他开发者发送邮件或者触发一个持续集成系统都是`post-receive`常用的操作。
这个脚本没有参数,但和`pre-receive`一样通过标准输入读取。 这个脚本没有参数,但和`pre-receive`一样通过标准输入读取。