简体   繁体   English

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

[英]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 checkoutgit 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-commitpre-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散列[ 3456 ]。 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' [ 56 ]。 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官方邮件列表上问了这些问题,到目前为止没有收到任何答案,真可惜......

Condensed 简明

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... 我建议深入潜水......

Better, but longer 更好,但更长

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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM