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 checkouts 和 git resets(由于 for 循环),以便使用 git-clang-format 单独分析所有推送的修订。我在这里试图避免的是对远程(服务器)裸存储库的推送访问的(可能的)并发问题。也就是说,我的印象是,如果多个开发人员将尝试同时推送到安装了此 pre-receive 挂钩的远程,那么如果每个推送 "sessions" 都不执行,则可能会导致问题git checkouts 和 git resets 及其存储库的私有副本。那么,简单来说,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-commitpre-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 等。对于(可能过于简单,但工作)上面的例子。我建议深入研究...

更好,但更长

一旦你明白了,我还建议你向下滚动到这个底部: