你想知道如何变得更漂亮。我将通过 post-receive 电子邮件挂钩脚本来展示它是如何工作的(部分)。这会变得很长!:-)
好的,所以,这是post-receieve-email
(重新格式化了一下)的关键部分:
while read oldrev newrev refname; do
prep_for_email $oldrev $newrev $refname || continue
generate_email $maxlines | send_mail
done
这会从输入流中读取 old-SHA、new-SHA 和 ref 名称(我认为这是 git 的失败之一:您只能读取此流一次,然后它就消失了;这使得连接一堆原本不相关的钩子过度困难)并调用两个 shell 函数来检查它们,然后忽略每个函数,或者对它们做一些事情。
现在这里是prep_for_email
shell 函数的关键部分,带有注释:
prep_for_email()
{
oldrev=$(git rev-parse $1)
newrev=$(git rev-parse $2)
refname="$3"
此处的 rev-parse(加上我在上面放置的一些允许命令行参数的代码)允许您提供诸如“HEAD”和“HEAD^”之类的东西 rev 名称。实际的接收后挂钩始终获取原始 SHA1,因此 rev-parse 调用是无操作的。
# --- Interpret
# 0000->1234 (create)
# 1234->2345 (update)
# 2345->0000 (delete)
if expr "$oldrev" : '0*$' >/dev/null
then
change_type="create"
else
if expr "$newrev" : '0*$' >/dev/null
then
change_type="delete"
else
change_type="update"
fi
fi
在 post-receive 钩子中,如果“旧”SHA1 是0000000000000000000000000000000000000000
(全零,即 40 个 0;expr
上面只是检查全零),这意味着“ref”参数以前不存在,现在存在。这通常是一个分支创建操作,但也可以是一个标签创建。另一方面,如果“新”SHA1 全部为零,则“ref”参数之前确实存在,现在不存在:通常是分支删除操作。其他任何东西,用于解析旧版本的引用名称现在解析为新版本。这通常是分支更新,但也可以是例如标签移动。
接下来,电子邮件挂钩有一些很好的“偏执狂风格”编程,交叉检查 ref-name 与 git repo 中的实际底层对象类型:
# --- Get the revision types
newrev_type=$(git cat-file -t $newrev 2> /dev/null)
oldrev_type=$(git cat-file -t "$oldrev" 2> /dev/null)
case "$change_type" in
create|update)
rev="$newrev"
rev_type="$newrev_type"
;;
delete)
rev="$oldrev"
rev_type="$oldrev_type"
;;
esac
如果 ref-name 具有表单refs/tags/*
并且更新是针对带注释的标记,则应该将$oldrev_type
和都设置$newrev_type
为tag
。(如果它是一个轻量级标签,它们都将是commit
。如果一个轻量级标签变成一个带注释的标签,旧类型将是提交,新类型将是标签,等等。)当然,如果你要删除一个分支或一个标签,“新” rev 将全为 0,并且$newrev_type
将是空字符串,因为git cat-file -t
它将简单地失败(这就是为什么2> /dev/null
)。
(除此之外:没有充分的理由git cat-file -t
引用它的论点,而一个没有。可能只是有人在遇到通常的空字符串参数问题后对引用过于满意,但错过了一个。幸运的是,在这种情况下,无论哪种方式都是无害的. :-) )
然后电子邮件脚本有一个很长的case
语句:
case "$refname","$rev_type" in
...
esac
这确保了一个操作refs/heads/*
始终是一个commit
. 如果没有,它将向 stderr 打印一条消息(在 a 上git push
,该消息将发送给进行推送的任何人,前缀为remote:
,以便有人可以看到它):
refs/heads/*,commit)
# branch
refname_type="branch"
short_refname=${refname##refs/heads/}
;;
...
*)
# Anything else (is there anything else?)
echo >&2 "*** Unknown type of update to $refname ($rev_type)"
echo >&2 "*** - no email generated"
return 1
;;
现在从generate_email
. 查看实际脚本以了解详细信息,但是,在这种情况下,我们只真正关心对 的调用generate_email_header
,以及调用 的情况generate_update_branch_email
。
这是标题:
generate_email_header()
{
# --- Email (all stdout will be the email)
# Generate header
cat <<-EOF
To: $recipients
Subject: ${emailprefix}$projectdesc $refname_type $short_refname ${change_type}d. $describe
X-Git-Refname: $refname
X-Git-Reftype: $refname_type
X-Git-Oldrev: $oldrev
X-Git-Newrev: $newrev
This is an automated email from the git hooks/post-receive script. It was
generated because a ref change was pushed to the repository containing
the project "$projectdesc".
The $refname_type, $short_refname has been ${change_type}d
EOF
}
$change_type
, 在我们关心的情况下, 是update
这样说的: branch zorg updated.
($describe
是 的输出git describe $newrev
, 或者如果它是空的 - 即没有注释标签git describe
可供使用 - 只是$newrev
. $recipients
, $emailprefix
, 并且$projectdesc
来自各种可配置项。)
在generate_update_branch_email
中,有很多东西要计算和打印以发送电子邮件,确切地说是删除和添加了哪些提交;然后以:
echo "Summary of changes:"
git diff-tree --stat --summary --find-copies-harder $oldrev..$newrev
基本上,$refname
形式的分支引用refs/heads/*
(比如,refs/heads/zorg
),已经是update
d(从不是全 0 的现有$oldrev
到不是全 0 的新$newrev
),意味着所说的(长格式)分支名称,人们通常称为$short_refname
( zorg
),已移动该分支尖端。这比找出这个动作的确切含义的代码要简单得多!
在您的情况下,您无需担心分支创建;您可以将其视为定期更新。您可能(或可能不)想要在删除分支时做一些特别的事情。大多数时候,您只关心分支更新……而您要做的就是,如果staging
已更新,则更新您的特殊暂存副本;如果master
已更新,请更新您的特殊master
副本。在每种情况下,更新都将是“到新的分支提示”,这真的很容易访问。
因此,如果我们将所有这些放在一起,我们会得到以下(未经测试,但非常简单)的钩子,我已经添加了从命令行调用它的能力。为了使它更有用,当从命令行调用时,您可以指定要在任何地方签出的修订版,例如,您可以说./hookscript unused master~2 refs/heads/staging
将 rev 推master~2
入暂存副本区域(假设您出于某种原因想要这样做)。
#! /bin/bash
#
# handle updates to our two interesting branches, staging and master.
# function to dump given commit state to target directory
# arguments: $1 - rev; $2 - target dir
copy_to_dir() {
GIT_WORK_TREE="$2" git checkout -q -f "$1"
}
# function to handle an update to staging branch.
# arguments: $1 - rev to check out
update_staging() {
copy_to_dir $1 /path/to/staging/site/webroot
}
# function to handle an update to master branch.
update_master() {
copy_to_dir $1 /var/www
}
# function to handle one reference-change.
# arguments:
# $1 - old revision, or all-0s on create
# $2 - new revision, or all-0s on removal
# $3 - reference (refs/heads/*, refs/tags/*, etc)
refchange() {
local oldrev="$1" newrev="$2" ref="$3"
local deleted=false
local short_revname
if expr "$newrev" : '0*$' >/dev/null; then
deleted=true
elif ! git rev-parse "$newrev"; then
return # git rev-parse already printed an error
fi
case $ref in
refs/heads/staging|refs/heads/master)
shortref=${refname#refs/heads/};;
*)
return;;
esac
# someone pushed a change to staging branch or master branch
if $deleted; then {
echo "WARNING: you've deleted branch $shortref"
echo "are you sure you wanted to do that?"
echo "The operating copy is still operating, and"
echo "will be updated when the branch is re-created."
} 1>&2
return
fi
# update either the staging copy or the master copy
update_$shortref "$newrev"
}
# main driver: update from input stream (if no arguments) or use arguments
case $# in
3) refchange "$1" "$2" "$3";;
0) while read oldrev newrev refname; do
refchange $oldrev $newrev $refname
done;;
*) echo "ERROR: update hook called with $# arguments, expected 0 or 3" 1>&2;;
esac
请注意,因为我git checkout -q -f
在已解析的 rev 上使用(而不是像staging
or之类的名称master
),所以每次都会将推送到的分支带到“分离的 HEAD”状态。更重要的是,如果您在 Web 服务器上使用“普通”存储库中的命令行技巧,它将解开您当前的分支。这不是致命的,但很容易令人讨厌。为避免这种情况,请将 的内容替换为copy_to_dir
,例如git archive $1 | (rm -rf "$2" && mkdir "$2" && cd "$2" && tar xf -)
.
(通常你会推送到一个--bare
仓库,改变它“当前分支”的想法不是问题,因为没有人关心这个。)