Git 'pre-receive' 钩子和 'git-clang-format' 脚本可靠地拒绝违反代码风格约定的推送
Git 'pre-receive' hook and 'git-clang-format' script to reliably reject pushes that violate code style conventions
让我们立即从我已经编写的 pre-receive
钩子的片段开始:
#!/bin/sh
##
format_bold='3[1m'
format_red='3[31m'
format_yellow='3[33m'
format_normal='3[0m'
##
format_error="${format_bold}${format_red}%s${format_normal}"
format_warning="${format_bold}${format_yellow}%s${format_normal}"
##
stdout() {
format=""
shift
printf "${format}" "${@}"
}
##
stderr() {
stdout "${@}" 1>&2
}
##
output() {
format=""
shift
stdout "${format}\n" "${@}"
}
##
error() {
format=""
shift
stderr "${format_error}: ${format}\n" 'error' "${@}"
}
##
warning() {
format=""
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
挂钩的远程,那么如果每个推送 "sessions" 都不执行,则可能会导致问题git checkout
s 和 git reset
s 及其存储库的私有副本。那么,简单来说,git-daemon
是否内置了并发推送的锁管理"sessions"?它会严格按顺序执行相应的 pre-receive
钩子实例还是有可能交错(这可能会导致未定义的行为)?有些东西告诉我,应该有一个内置的解决方案来解决这个问题,并提供具体的保证,否则遥控器通常如何工作(即使没有复杂的挂钩)受到并发推送的影响?如果有这样的内置解决方案,那么副本就是多余的,简单地重用裸存储库实际上会加快处理速度。顺便说一下,非常欢迎任何关于这个问题的官方文档参考。
其次,git-clang-format
仅处理 staged(但未提交)更改与特定提交(默认为 HEAD
)。因此,您可以轻松地看到角落案例所在的位置。是的,它与 root 提交(修订)有关。事实上,git reset --soft 'HEAD~1'
不能应用于 root 提交,因为它们没有要重置的父项。因此,对我的第二个问题进行以下检查:
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
无法再处理它。我认为这与以下事实有关,即所有这些正在分析的推送修订(包括这个根修订)并不真正属于任何分支。也就是说,它们处于 detached HEAD
状态。如果能找到解决这个极端情况的方法也是完美的,这样 initial 提交也可以通过 git-clang-format
进行相同的检查以符合代码风格约定。
和平。
注意:
对于那些寻找最新的、(或多或少)全面且经过良好测试的解决方案的人,我托管了相应的 public 存储库 [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 邮件列表上问了这些问题,但到目前为止没有收到任何答复,真可惜...
浓缩
我在理解第一个示例时遇到了一些麻烦,部分原因是它的长度和额外的花絮使其对 OP 的特定用例很有用。我梳理并浓缩为:
ref_name=
new_rev=
# 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-format、eslint 等。对于(可能过于简单,但工作)上面的例子。我建议深入研究...
更好,但更长
一旦你明白了,我还建议你向下滚动到这个底部:
让我们立即从我已经编写的 pre-receive
钩子的片段开始:
#!/bin/sh
##
format_bold='3[1m'
format_red='3[31m'
format_yellow='3[33m'
format_normal='3[0m'
##
format_error="${format_bold}${format_red}%s${format_normal}"
format_warning="${format_bold}${format_yellow}%s${format_normal}"
##
stdout() {
format=""
shift
printf "${format}" "${@}"
}
##
stderr() {
stdout "${@}" 1>&2
}
##
output() {
format=""
shift
stdout "${format}\n" "${@}"
}
##
error() {
format=""
shift
stderr "${format_error}: ${format}\n" 'error' "${@}"
}
##
warning() {
format=""
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
挂钩的远程,那么如果每个推送 "sessions" 都不执行,则可能会导致问题git checkout
s 和 git reset
s 及其存储库的私有副本。那么,简单来说,git-daemon
是否内置了并发推送的锁管理"sessions"?它会严格按顺序执行相应的 pre-receive
钩子实例还是有可能交错(这可能会导致未定义的行为)?有些东西告诉我,应该有一个内置的解决方案来解决这个问题,并提供具体的保证,否则遥控器通常如何工作(即使没有复杂的挂钩)受到并发推送的影响?如果有这样的内置解决方案,那么副本就是多余的,简单地重用裸存储库实际上会加快处理速度。顺便说一下,非常欢迎任何关于这个问题的官方文档参考。
其次,git-clang-format
仅处理 staged(但未提交)更改与特定提交(默认为 HEAD
)。因此,您可以轻松地看到角落案例所在的位置。是的,它与 root 提交(修订)有关。事实上,git reset --soft 'HEAD~1'
不能应用于 root 提交,因为它们没有要重置的父项。因此,对我的第二个问题进行以下检查:
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
无法再处理它。我认为这与以下事实有关,即所有这些正在分析的推送修订(包括这个根修订)并不真正属于任何分支。也就是说,它们处于 detached HEAD
状态。如果能找到解决这个极端情况的方法也是完美的,这样 initial 提交也可以通过 git-clang-format
进行相同的检查以符合代码风格约定。
和平。
注意:
对于那些寻找最新的、(或多或少)全面且经过良好测试的解决方案的人,我托管了相应的 public 存储库 [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 邮件列表上问了这些问题,但到目前为止没有收到任何答复,真可惜...
浓缩
我在理解第一个示例时遇到了一些麻烦,部分原因是它的长度和额外的花絮使其对 OP 的特定用例很有用。我梳理并浓缩为:
ref_name=
new_rev=
# 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-format、eslint 等。对于(可能过于简单,但工作)上面的例子。我建议深入研究...
更好,但更长
一旦你明白了,我还建议你向下滚动到这个底部: