Git 'pre-receive' 钩子和 'git-clang-format' 脚本可靠地拒绝违反代码样式约定的推送
Posted
技术标签:
【中文标题】Git \'pre-receive\' 钩子和 \'git-clang-format\' 脚本可靠地拒绝违反代码样式约定的推送【英文标题】:Git 'pre-receive' hook and 'git-clang-format' script to reliably reject pushes that violate code style conventionsGit 'pre-receive' 钩子和 'git-clang-format' 脚本可靠地拒绝违反代码样式约定的推送 【发布时间】:2016-02-28 17:10:55 【问题描述】:让我们立即从我已经编写的 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
注意:
代码不相关的地方用# ...
存根。
注意:
如果你不熟悉git-clang-format
,可以看看here。
该钩子按预期工作,到目前为止,我没有发现任何错误,但如果您发现任何问题或有改进建议,我将不胜感激任何报告。也许,我应该评论一下这个钩子背后的意图是什么。好吧,它确实使用git-clang-format
检查每个推送的修订是否符合代码样式约定,如果其中任何一个不符合,它将为每个版本输出相关的差异(告诉开发人员应该修复什么的差异)。基本上,我对这个钩子有两个深入的问题。
首先,请注意我将远程(服务器)的裸存储库复制到某个临时目录,并在其中签出代码以进行分析。让我解释一下这样做的意图。请注意,我做了几个git checkout
s 和git reset
s(由于for
循环),以便使用git-clang-format
单独分析所有推送的修订。我在这里要避免的是推送访问远程(服务器)裸存储库时的(可能的)并发问题。也就是说,我的印象是,如果多个开发人员将尝试同时向安装了此 pre-receive
钩子的远程推送,那么如果这些推送“会话”中的每一个都不执行 git checkout
s,则可能会导致问题和 git reset
s 及其存储库的私有副本。那么,简单来说,git-daemon
是否有针对并发推送“会话”的内置锁管理?它会严格按顺序执行相应的pre-receive
钩子实例,还是有可能发生交错(这可能会导致未定义的行为)?有些东西告诉我,应该有一个内置的解决方案来解决这个问题,并提供具体的保证,否则远程如何在一般情况下(即使没有复杂的钩子)受到并发推送?如果有这样的内置解决方案,那么副本是多余的,简单地重用裸存储库实际上会加快处理速度。顺便说一句,非常欢迎任何关于这个问题的官方文档的参考。
其次,git-clang-format
仅处理暂存(但未提交)更改与特定提交(默认为HEAD
)。因此,您可以轻松查看角落案例所在的位置。是的,它与 root 提交(修订)有关。事实上,git reset --soft 'HEAD~1'
不能应用于根提交,因为它们没有要重置的父级。因此,对我的第二个问题进行以下检查:
if [ "$(list --count "$sha1")" -eq 1 ]; then
# What should I put here?
else
git reset --soft 'HEAD~1' > '/dev/null' 2>&1
fi
我已经尝试过git update-ref -d 'HEAD'
,但这会破坏存储库,导致git-clang-format
无法再处理它。我相信这与所有这些正在分析的推送修订(包括这个根)并不真正属于任何分支的事实有关。也就是说,它们处于分离HEAD
状态。为这种极端情况找到一个解决方案也是完美的,这样 initial 提交也可以通过git-clang-format
进行相同的检查,以符合代码样式约定。
和平。
【问题讨论】:
【参考方案1】:注意:
对于那些正在寻找最新、(或多或少)全面且经过良好测试的解决方案的人,我托管了相应的公共存储库 [1]。目前实现了依赖git-clang-format
的两个重要钩子:pre-commit
和pre-receive
。理想情况下,当同时使用它们时,您将获得最自动化和最简单的工作流程。像往常一样,非常欢迎改进建议。
注意:
目前,pre-commit
钩子 [1] 需要将 git-clang-format.diff
补丁(也是我编写的)[1] 应用于git-clang-format
。此补丁的动机和用例示例在提交给 LLVM/Clang [2] 的官方补丁审查中进行了总结。希望它很快会被上游接受并合并。
我已经设法为第二个问题实施了解决方案。我不得不承认,由于 Git 文档稀少且没有示例,所以很难找到它。我们先来看看对应的代码改动:
# ...
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
# ...
如您所见,我现在明确指示git-clang-format
使用--commit
选项对HEAD~1
进行操作,而不是重复执行git reset --soft 'HEAD~1'
(而它的默认值是HEAD
,这在初始版本中是隐含的)在我的问题中提出)。但是,这仍然不能单独解决问题,因为当我们点击 root 提交时,这将再次导致错误,因为 HEAD~1
将不再引用有效的修订版(类似于它会不可能做到git reset --soft 'HEAD~1'
)。这就是为什么对于这种特殊情况,我指示git-clang-format
对(魔术)4b825dc642cb6eb9a060e54bf8d69288fbee4904
哈希 [3,4,5,6] 进行操作。要了解有关此哈希的更多信息,请查阅参考资料,但简而言之,它指的是 Git 空树对象——没有任何暂存或提交的对象,这正是我们需要的 git-clang-format
在我们的案例中进行操作。
注意:
您不必牢记4b825dc642cb6eb9a060e54bf8d69288fbee4904
,最好不要对其进行硬编码(以防万一这个神奇的哈希值在未来发生变化)。事实证明,它总是可以用git hash-object -t tree '/dev/null'
[5, 6] 来检索。因此,在上述pre-receive
钩子的最终版本中,我使用了commit="$(git hash-object -t tree '/dev/null')"
。
P.S.我仍在寻找关于我的第一个问题的高质量答案。顺便说一句,我在 Git 官方邮件列表上问了这些问题,至今没有收到任何答复,真可惜……
【讨论】:
【参考方案2】:精简
我在理解第一个示例时遇到了一点麻烦,部分原因是长度和额外的花絮使其对 OP 的特定用例有用。我梳理了一下,浓缩成这样:
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
此示例使用 Prettier,但它可以很好地映射到 clang 格式、eslint 等。上面的示例(可能过于简单,但有效)有一些限制。我建议潜水更深...
更好,但更长
一旦您了解了这一点,我还建议您向下滚动到这个底部:
Reject Ugly Commits with Server-Side Git Hooks【讨论】:
能否也将链接文本添加到您的消息中? @BrunoAlexandreRosa 在我看来,在这里重新发布完整的博客文章太长了。您认为这里有什么具体的内容吗?以上是关于Git 'pre-receive' 钩子和 'git-clang-format' 脚本可靠地拒绝违反代码样式约定的推送的主要内容,如果未能解决你的问题,请参考以下文章
[转] 利用git钩子,使用python语言获取提交的文件列表
Git push提示pre-receive hook declined