Git'预先接收'钩子和'git-clang-format'脚本可以可靠地拒绝违反代码样式约定的推送

Ale*_*aev 11 git sh githooks git-daemon clang-format

让我们立刻从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
Run Code Online (Sandbox Code Playgroud)

注意:
带有不相关代码的地方都有# ....

注意:
如果您不熟悉git-clang-format,请查看此处.

这个钩子按预期工作,到目前为止,我没有注意到任何错误,但如果你发现任何问题或有改进建议,我会很感激任何报告.也许,我应该评论这个钩子背后的意图.好吧,它会检查每个推送的修订版本是否符合代码样式约定git-clang-format,如果它们中的任何一个不符合,它将输出相关的差异(告诉开发人员应该修复的内容).基本上,我有两个关于这个钩子的深入问题.

首先,请注意我将远程(服务器)裸存储库的副本执行到某个临时目录,并检查代码以进行分析.让我解释一下这个意图.请注意,我执行了几个git checkouts和git resets(由于for循环),以便单独分析所有推送的修订git-clang-format.我在这里要避免的是推送访问远程(服务器)裸存储库时的(可能的)并发问题.也就是说,我的印象是,如果多个开发人员尝试同时推送到pre-receive安装了此挂钩的远程控制器,那么如果这些推送"会话"中的每一个都没有使用其私有副本执行git checkouts和git resets ,则可能会导致问题存储库.那么,简单来说,是否git-daemon有内置的锁管理用于并发推送"会话"?它是否会pre-receive严格按顺序执行相应的钩子实例,或者是否存在交错的可能性(这可能会导致未定义的行为)?有些东西告诉我应该有一个内置的解决方案来解决这个问题的具体保证,否则遥控器如何工作(即使没有复杂的钩子)会受到并发推送?如果有这样的内置解决方案,那么副本就是多余的,只需重新使用裸存储库就可以加快处理速度.顺便说一下,非常欢迎任何有关这个问题的官方文件的参考.

其次,git-clang-format进程仅暂存(但未提交)更改与特定提交(HEAD默认情况下).因此,您可以轻松查看角落的位置.是的,它是提交(修订).实际上,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
Run Code Online (Sandbox Code Playgroud)

我已经尝试了git update-ref -d 'HEAD'但是这会以一种git-clang-format无法再处理它的方式打破存储库.我相信这与所有这些正在分析的修订版(包括这个根修订版)并不真正属于任何分支的事实有关.也就是说,他们处于分离 HEAD状态.找到这个角落案例的解决方案也是完美的,因此初始提交也可以进行相同的检查,git-clang-format以符合代码样式约定.

和平.

Ale*_*aev 5

注意:
对于那些寻找最新(或多或少)全面且经过良好测试的解决方案的人,我将托管相应的公共存储库[ 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
      # ...
Run Code Online (Sandbox Code Playgroud)

正如您所看到的,git reset --soft 'HEAD~1'我现在明确地指示使用该选项git-clang-format进行操作(而不是在我的问题中提供的初始版本中隐含的默认值),而不是反复执行.但是,这仍然没有解决问题本身,因为当我们点击root提交时,这将再次导致错误,因为不再引用有效修订(类似于不可能这样做).这就是为什么此特定情况下,我指示兑(魔术)操作散列[ 3,4,5,6 ].要了解有关此哈希的更多信息,请查阅引用,但是,简而言之,它引用了Git 空树对象 - 没有暂存或提交的对象,这正是我们需要在我们的情况下操作的对象.HEAD~1--commitHEADHEAD~1git reset --soft 'HEAD~1'git-clang-format4b825dc642cb6eb9a060e54bf8d69288fbee4904git-clang-format

注意:
你不必刻意记住4b825dc642cb6eb9a060e54bf8d69288fbee4904它,最好不要硬编码(以防万一这个神奇的哈希值将来会发生变化).事实证明,它可以随时与被检索git hash-object -t tree '/dev/null'[ 5,6 ].因此,在我上面的pre-receive钩子的最终版本中,我有commit="$(git hash-object -t tree '/dev/null')".

PS 我仍然在寻找第一个问题的优质答案.顺便说一句,我在Git官方邮件列表上问了这些问题,到目前为止没有收到任何答案,真可惜......