[英]Git 'pre-receive' hook and 'git-clang-format' script to reliably reject pushes that violate code style conventions
Let's immediately start with a scrap of the pre-receive
hook that I've already written: 让我们立即从我已经写过的
pre-receive
钩子开始:
#!/bin/sh
##
format_bold='\033[1m'
format_red='\033[31m'
format_yellow='\033[33m'
format_normal='\033[0m'
##
format_error="${format_bold}${format_red}%s${format_normal}"
format_warning="${format_bold}${format_yellow}%s${format_normal}"
##
stdout() {
format="${1}"
shift
printf "${format}" "${@}"
}
##
stderr() {
stdout "${@}" 1>&2
}
##
output() {
format="${1}"
shift
stdout "${format}\n" "${@}"
}
##
error() {
format="${1}"
shift
stderr "${format_error}: ${format}\n" 'error' "${@}"
}
##
warning() {
format="${1}"
shift
stdout "${format_warning}: ${format}\n" 'warning' "${@}"
}
##
die() {
error "${@}"
exit 1
}
##
git() {
command git --no-pager "${@}"
}
##
list() {
git rev-list "${@}"
}
##
clang_format() {
git clang-format --style='file' "${@}"
}
##
while read sha1_old sha1_new ref; do
case "${ref}" in
refs/heads/*)
branch="$(expr "${ref}" : 'refs/heads/\(.*\)')"
if [ "$(expr "${sha1_new}" : '0*$')" -ne 0 ]; then # delete
unset sha1_new
# ...
else # update
if [ "$(expr "${sha1_old}" : '0*$')" -ne 0 ]; then # create
unset sha1_old
sha1_range="${sha1_new}"
else
sha1_range="${sha1_old}..${sha1_new}"
# ...
fi
fi
# ...
GIT_WORK_TREE="$(mktemp --tmpdir -d 'gitXXXXXX')"
export GIT_WORK_TREE
GIT_DIR="${GIT_WORK_TREE}/.git"
export GIT_DIR
mkdir -p "${GIT_DIR}"
cp -a * "${GIT_DIR}/"
ln -s "${PWD}/../.clang-format" "${GIT_WORK_TREE}/"
error=
for sha1 in $(list "${sha1_range}"); do
git checkout --force "${sha1}" > '/dev/null' 2>&1
if [ "$(list --count "${sha1}")" -eq 1 ]; then
# What should I put here?
else
git reset --soft 'HEAD~1' > '/dev/null' 2>&1
fi
diff="$(clang_format --diff)"
if [ "${diff%% *}" = 'diff' ]; then
error=1
error '%s: %s\n%s' \
'Code style issues detected' \
"${sha1}" \
"${diff}" \
1>&2
fi
done
if [ -n "${error}" ]; then
die '%s' 'Code style issues detected'
fi
fi
;;
refs/tags/*)
tag="$(expr "${ref}" : 'refs/tags/\(.*\)')"
# ...
;;
*)
# ...
;;
esac
done
exit 0
NOTE: 注意:
Places with irrelevant code are stubbed with # ...
. 带有不相关代码的地方以
# ...
为标记。
NOTE: 注意:
If you are not familiar with git-clang-format
, take a look here . 如果您不熟悉
git-clang-format
,请查看此处 。
That hook works as expected, and so far, I didn't notice any bugs, but if you spot any problem or have an improvement suggestion, I'd appreciate any report. 这个钩子按预期工作,到目前为止,我没有注意到任何错误,但如果你发现任何问题或有改进建议,我会很感激任何报告。 Probably, I should give a comment on what's the intention behind this hook.
也许,我应该评论这个钩子背后的意图。 Well, it does check every pushed revision for compliance with code style conventions using
git-clang-format
, and if any of them does not comply, it will output the relevant diff (the one telling developers what should be fixed) for each of them. 好吧,它确实使用
git-clang-format
检查每个推送修订是否符合代码样式约定,如果它们中的任何一个不符合,它将输出相关的差异(告诉开发人员应该修复的内容)。 。 Basically, I have two in-depth questions regarding this hook. 基本上,我有两个关于这个钩子的深入问题。
First, notice that I perform copy of the remote's (server) bare repository to some temporary directory and check out the code for analysis there. 首先,请注意我将远程(服务器)裸存储库的副本执行到某个临时目录,并检查代码以进行分析。 Let me explain the intention of this.
让我解释一下这个意图。 Note that I do several
git checkout
s and git reset
s (due to for
loop) in order to analyze all of the pushed revisions individually with git-clang-format
. 请注意,我执行了几个
git checkout
和git reset
s(由于for
循环),以便使用git-clang-format
分别分析所有推送的修订。 What I am trying to avoid here, is the (possible) concurrency issue on push access to the remote's (server) bare repository. 我在这里要避免的是推送访问远程(服务器)裸存储库时的(可能的)并发问题。 That is, I'm under impression that if multiple developers will try to push at the same time to a remote with this
pre-receive
hook installed, that might cause problems if each of these push "sessions" does not do git checkout
s and git reset
s with its private copy of the repository. 也就是说,我的印象是,如果多个开发人员试图同时将这个
pre-receive
挂钩安装到远程控制器,如果这些推送“会话”中的每一个都没有执行git checkout
,那么这可能会导致问题。 git reset
s及其存储库的私有副本。 So, to put it simple, does git-daemon
have built-in lock management for concurrent push "sessions"? 那么,简单来说,
git-daemon
是否有内置的锁管理用于并发推送“会话”? Will it execute the corresponding pre-receive
hook instances strictly sequentially or there is a possibility of interleaving (which can potentially cause undefined behavior)? 它
pre-receive
严格按顺序执行相应的pre-receive
挂钩实例,还是存在交错的可能性(可能导致未定义的行为)? Something tells me that there should be a built-in solution for this problem with concrete guarantees, otherwise how would remotes work in general (even without complex hooks) being subjected to concurrent pushes? 有些东西告诉我应该有一个内置的解决方案来解决这个问题的具体保证,否则遥控器如何工作(即使没有复杂的钩子)会受到并发推送? If there is such a built-in solution, then the copy is redundant and simply reusing the bare repository would actually speed up the processing.
如果有这样的内置解决方案,那么副本就是多余的,只需重新使用裸存储库就可以加快处理速度。 By the way, any reference to official documentation regarding this question is very welcome.
顺便说一下,非常欢迎任何有关这个问题的官方文件的参考。
Second, git-clang-format
processes only staged (but not committed) changes vs. specific commit ( HEAD
by default). 其次,
git-clang-format
进程只进行了阶段 (但未提交)更改与特定提交(默认情况下为HEAD
)。 Thus, you can easily see where a corner case lies. 因此,您可以轻松查看角落所在的位置。 Yes, it's with the root commits (revisions).
是的,它是根提交(修订)。 In fact,
git reset --soft 'HEAD~1'
cannot be applied to root commits as they have no parents to reset to. 实际上,
git reset --soft 'HEAD~1'
不能应用于root提交,因为它们没有父级要重置。 Hence, the following check with my second question is there: 因此,我的第二个问题的以下检查是:
if [ "$(list --count "${sha1}")" -eq 1 ]; then
# What should I put here?
else
git reset --soft 'HEAD~1' > '/dev/null' 2>&1
fi
I've tried git update-ref -d 'HEAD'
but this breaks the repository in such a way that git-clang-format
is not able to process it anymore. 我已经尝试过
git update-ref -d 'HEAD'
但这会破坏存储库,使得git-clang-format
无法再处理它。 I believe this is related to the fact that all of these pushed revisions that are being analyzed (including this root one) do not really belong to any branch yet. 我相信这与以下事实有关:所有这些正在分析的修订版(包括这个修订版)并不真正属于任何分支。 That is, they are in detached
HEAD
state. 也就是说,它们处于分离的
HEAD
状态。 It would be perfect to find a solution to this corner case as well, so that initial commits can also undergo the same check by git-clang-format
for compliance with code style conventions. 找到这个角落案例的解决方案也是完美的,因此初始提交也可以通过
git-clang-format
进行相同的检查,以符合代码样式约定。
Peace. 和平。
NOTE: 注意:
For those looking for an up-to-date, (more or less) comprehensive, and well-tested solution, I host the corresponding public repository [ 1 ]. 对于那些寻找最新(或多或少)全面且经过良好测试的解决方案的人,我将托管相应的公共存储库[ 1 ]。 Currently, the two important hooks relying on
git-clang-format
are implemented: pre-commit
and pre-receive
. 目前,实现了依赖于
git-clang-format
的两个重要钩子: pre-commit
和pre-receive
。 Ideally, you get the most automation and fool-proof workflow when using both of them simultaneously. 理想情况下,当您同时使用它们时,您可以获得最自动化和最简单的工作流程。 As usual, improvement suggestions are very welcome.
像往常一样,非常欢迎改进建议。
NOTE: 注意:
Currently, the pre-commit
hook [ 1 ] requires the git-clang-format.diff
patch (authored by me as well) [ 1 ] to be applied to git-clang-format
. 目前,
pre-commit
hook [ 1 ]需要将git-clang-format.diff
补丁(我也是由他创作)[ 1 ]应用于git-clang-format
。 The motivation and use case examples for this patch are summarized in the official patch review submission to LLVM/Clang [ 2 ]. 这个补丁的动机和用例示例总结在LLVM / Clang [ 2 ]的官方补丁审查提交中。 Hopefully, it will be accepted and merged upstream soon.
希望它很快就会被接受并合并到上游。
I've managed to implement a solution for the second question. 我已设法为第二个问题实施解决方案。 I have to admit that it was not easy to find due to scarce Git documentation and absence of examples.
我不得不承认,由于缺乏Git文档和缺少示例,因此不容易找到。 Let's take a look at the corresponding code changes first:
我们先来看看相应的代码更改:
# ...
clang_format() {
git clang-format --commit="${commit}" --style='file' "${@}"
}
# ...
for sha1 in $(list "${sha1_range}"); do
git checkout --force "${sha1}" > '/dev/null' 2>&1
if [ "$(list --count "${sha1}")" -eq 1 ]; then
commit='4b825dc642cb6eb9a060e54bf8d69288fbee4904'
else
commit='HEAD~1'
fi
diff="$(clang_format --diff)"
# ...
done
# ...
As you can see, instead of repeatedly doing git reset --soft 'HEAD~1'
, I now explicitly instruct git-clang-format
to operate against HEAD~1
with the --commit
option (whereas its default is HEAD
that was implied in the initial version presented in my question). 正如你所看到
git reset --soft 'HEAD~1'
,我现在明确指示git-clang-format
使用--commit
选项对HEAD~1
进行操作,而不是反复进行git reset --soft 'HEAD~1'
,而它的默认值是隐含的HEAD
在我的问题中提出的初始版本)。 However, that still does not solve the problem on its own because when we would hit root commit this would again result in error as HEAD~1
would not refer to a valid revision anymore (similarly to how it would not be possible to do git reset --soft 'HEAD~1'
). 但是,这仍然无法自行解决问题,因为当我们点击root提交时,这将再次导致错误,因为
HEAD~1
将不再引用有效修订版(类似于不可能执行git reset --soft 'HEAD~1'
)。 That's why for this particular case, I instruct git-clang-format
to operate against the (magic) 4b825dc642cb6eb9a060e54bf8d69288fbee4904
hash [ 3 , 4 , 5 , 6 ]. 这就是为什么此特定情况下,我指示
git-clang-format
抵靠(魔术)操作4b825dc642cb6eb9a060e54bf8d69288fbee4904
散列[ 3 , 4 , 5 , 6 ]。 To learn more about this hash, consult the references, but, in brief, it refers to the Git empty tree object — the one that has nothing staged or committed, which is exactly what we need git-clang-format
to operate against in our case. 要了解有关此哈希的更多信息,请参阅引用,但是,简而言之,它引用了Git 空树对象 - 没有暂存或提交的对象,这正是我们需要
git-clang-format
来操作的对象。案件。
NOTE: 注意:
You don't have to remember 4b825dc642cb6eb9a060e54bf8d69288fbee4904
by heart and it's better not to hard code it (just in case this magic hash ever changes in future). 你不必记住
4b825dc642cb6eb9a060e54bf8d69288fbee4904
,最好不要对它进行硬编码(以防万一这个神奇的哈希值将来会发生变化)。 It turns out that it can always be retrieved with git hash-object -t tree '/dev/null'
[ 5 , 6 ]. 事实证明,它可以随时与被检索
git hash-object -t tree '/dev/null'
[ 5 , 6 ]。 Thus, in my final version of the above pre-receive
hook, I have commit="$(git hash-object -t tree '/dev/null')"
instead. 因此,在我上面的
pre-receive
挂钩的最终版本中,我有commit="$(git hash-object -t tree '/dev/null')"
。
PS I'm still looking for a good quality answer on my first question. PS我在第一个问题上仍然在寻找一个高质量的答案。 By the way, I asked these questions on the official Git mailing list and received no answers so far, what a shame...
顺便说一句,我在Git官方邮件列表上问了这些问题,到目前为止没有收到任何答案,真可惜......
I had a little bit of trouble understanding the first example, in part due to the length and extra tidbits that make it useful for the OP's specific use case. 我在理解第一个例子时遇到了一些麻烦,部分是由于长度和额外的花絮使得它对OP的特定用例有用。 I combed through and condensed it down to this:
我仔细研究并将其浓缩为:
ref_name=$1
new_rev=$3
# only check branches, not tags or bare commits
if [ -z $(echo $ref_name | grep "refs/heads/") ]; then
exit 0
fi
# don't check empty branches
if [ "$(expr "${new_rev}" : '0*$')" -ne 0 ]; then
exit 0
fi
# Checkout a copy of the branch (but also changes HEAD)
my_work_tree=$(mktemp -d -t git-work-tree.XXXXXXXX) 2>/dev/null
git --work-tree="${my_work_tree}" --git-dir="." checkout $new_rev -f >/dev/null
# Do the formatter check
echo "Checking code formatting..."
pushd ${my_work_tree} >/dev/null
prettier './**/*.{js,css,html,json,md}' --list-different
my_status=$?
popd >/dev/null
# reset HEAD to master, and cleanup
git --work-tree="${my_work_tree}" --git-dir="." checkout master -f >/dev/null
rm -rf "${my_work_tree}"
# handle error, if any
if [ "0" != "$my_status" ]; then
echo "Please format the files listed above and re-commit."
echo "(and don't forget your .prettierrc, if you have one)"
exit 1
fi
This example is using Prettier, but it'll map pretty well to clang-format, eslint, etc. There are a few limitations to the (perhaps oversimplified, but working) example above. 这个例子使用的是Prettier,但是它会很好地映射到clang-format,eslint等。上面的例子(可能过于简单,但有效)有一些限制 。 I'd recommend diving deeper...
我建议深入潜水......
Once you've grok'd that I'd also recommend taking a scroll down towards the bottom of this one: 一旦你弄清楚了,我还建议你向下滚动到这个底部:
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.