Git'pre-receive'钩子和'git-clang-format'脚本可靠地拒绝违反代码风格惯例的推送 [英] Git 'pre-receive' hook and 'git-clang-format' script to reliably reject pushes that violate code style conventions

查看:455
本文介绍了Git'pre-receive'钩子和'git-clang-format'脚本可靠地拒绝违反代码风格惯例的推送的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述



<$ p

$ p> #!/ 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}
转换
printf$ {format}$ {@}
}
##
stderr(){
stdout$ {@} 1> 2
}
##
output(){
format =$ {1}
shift
stdout$ {格式} \\\
$ {@}
}
##
error(){
format =$ {1}
shift
stderr$ {format_error}:$ {format} \\\
'error'$ {@}
}
##
warning(){
format =$ {1}
shift
stdout$ {format_warning}:$ {format} \\\
'warning'$ {@}
}
# #
die(){
error$ {@}
exit 1
}
##
git(){
command git --no-pager$ {@}
}
##
list(){
git rev-list$ {@}
}
##
clang_format(){
git clang-format --style ='file'$ {@}
}
##
while阅读sha1_old sha1_new ref;在
refs / heads / *)
branch =$(expr$ {ref}:'refs / heads / \(。*)中做
case$ {ref}。 \\')')
if [$(expr$ {sha1_new}:'0 * $')-ne 0];然后#删除
unset sha1_new
#...
else#update
if [$(expr$ {sha1_old}:'0 * $')-ne 0];然后#创建
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});做
git checkout --force$ {sha1}> '/ dev / null'2& 1
if [$(list --count$ {sha1})-eq 1];那么
#我应该在这里放什么?
else
git reset --soft'HEAD〜1'> '/ dev / null'2& 1
fi
diff =$(clang_format --diff)
if [$ {diff %% *}='diff' ]。然后
错误= 1
错误'%s:%s \%s'\
'检测到代码样式问题'\
$ {sha1}\\ \\
$ {diff}\
1>& 2
fi
完成
if [-n$ {error}];然后
死'%s''检测到代码样式问题'
fi
fi
;;
refs / tags / *)
tag =$(expr$ {ref}:'refs / tags / \(。* \)')
#.. 。
;;
*)
#...
;;
esac
完成
出口0

注意:









$ b $ > 注意:

如果您不熟悉 git-clang-format ,请看这里



该钩子按预期工作,到目前为止,我没有注意到任何错误,但如果您发现任何问题或有改进建议,我会很感激任何报告。也许,我应该评论一下这个钩子背后的意图。那么,它会使用 git-clang-format 检查每个推送的修订是否符合代码风格约定,如果其中任何一个不符合,它将输出相关的diff一个告诉开发者应该固定的东西)。基本上,我对这个钩子有两个深入的问题。首先,请注意,我将远程(服务器)裸仓库的副本执行到某个临时目录并检出在那里分析代码。让我解释一下这个的意图。注意我做了几个 git checkout s和 git reset s(由于 code> loop),以便用 git-clang-format 分别分析所有推送的修订版本。我在这里试图避免的是对远程(服务器)裸仓库的push访问(可能)的并发问题。也就是说,我的印象是,如果多个开发人员尝试将安装的 pre-receive 挂钩同时推送到远程,那么可能会导致问题,如果每个这些推送会话不会使用其存储库的专用副本执行 git checkout s和 git reset s。因此,简单来说, git-daemon 是否具有对并发推送会话的内置锁管理?它是否会严格按顺序执行相应的预接收钩子实例,或者存在交织的可能性(这可能会导致未定义的行为)?有些东西告诉我应该有一个内置的解决方案来解决这个问题并提供具体的保证,否则遥控器一般会如何工作(即使没有复杂的钩子)也会受到同时推送?如果有这样的内置解决方案,那么副本是多余的,只是简单地重新使用裸存储库实际上会加速处理。顺便说一句,任何有关这个问题的官方文档的引用是非常受欢迎的。

其次, git-clang-format 仅处理 staged (但未提交)更改与特定提交(默认情况下为 HEAD )。因此,您可以轻松查看角落案例所在的位置。是的,它与提交(修订版)。事实上, git reset --soft'HEAD〜1'无法应用于根提交,因为它们没有父级重置为。因此,下面检查我的第二个问题:

  if [$(list --count$ {sha1} )-eq 1];那么
#我应该在这里放什么?
else
git reset --soft'HEAD〜1'> '/ dev / null'2& 1
fi

code> git update-ref -d'HEAD'但这会破坏版本库,使得 git-clang-format 是无法处理它了。我认为这与所有这些推动正在分析的修订(包括这个根源)并不属于任何分支的事实有关。也就是说,它们在 detached HEAD 状态中。最好找到这个角落案例的解决方案,以便 initial 提交也可以通过 git-clang-format 符合代码风格约定。



和平。

解决方案

注意:

对于那些寻找最新(或多或少)全面且经过充分测试的解决方案的人,我将相应的公共存储库[ 1 ]。目前,依赖于 git-clang-format 的两个重要钩子被实现: pre-commit 预接收。理想情况下,您在同时使用这两种工作流程时,可以获得最自动化和防呆的工作流程。像往常一样,改进建议非常受欢迎。

注意:

目前, pre -commit hook [ 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 $(列表$ {sha1_range});做
git checkout --force$ {sha1}> '/ dev / null'2& 1
if [$(list --count$ {sha1})-eq 1];然后
commit ='4b825dc642cb6eb9a060e54bf8d69288fbee4904'
else
commit ='HEAD〜1'
fi
diff =$(clang_format --diff)
#...
完成
#...

正如您所见,而不是重复执行 git reset --soft'HEAD〜1',我现在明确指示 git-clang-format 使用 - commit 选项对 HEAD〜1 进行操作(而其默认值是 HEAD 这是在我的问题中提出的初始版本中隐含的)。然而,这仍然不能自行解决问题,因为当我们击中 root 时,这会再次导致错误,因为 HEAD〜1 会不再引用有效的修订版(类似于不可能执行 git reset --soft'HEAD〜1')。这就是为什么在这个特殊情况下,我指示 git-clang-format 来操作(magic) 4b825dc642cb6eb9a060e54bf8d69288fbee4904 hash [ 3 4 5 ,< a href =http://comments.gmane.org/gmane.comp.version-control.git/180511 =nofollow noreferrer> 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')改为



PS 我仍然在寻找优质的答案题。顺便说一下,我在官方的Git邮件列表中提出了这些问题,但迄今还没有收到任何答案,真是太遗憾了......


Let's immediately start with a scrap of the pre-receive hook that I've already written:

#!/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.

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. 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 checkouts and git resets (due to for loop) in order to analyze all of the pushed revisions individually with 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 checkouts and git resets with its private copy of the repository. So, to put it simple, does git-daemon have built-in lock management for concurrent push "sessions"? Will it execute the corresponding pre-receive hook instances strictly sequentially or there is a possibility of interleaving (which can potentially cause undefined behavior)? 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). 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. 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. 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. 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.

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]. Currently, the two important hooks relying on git-clang-format are implemented: pre-commit and pre-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. The motivation and use case examples for this patch are summarized in the official patch review submission to 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. 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). 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'). That's why for this particular case, I instruct git-clang-format to operate against the (magic) 4b825dc642cb6eb9a060e54bf8d69288fbee4904 hash [3, 4, 5, 6]. 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.

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). It turns out that it can always be retrieved with git hash-object -t tree '/dev/null' [5, 6]. Thus, in my final version of the above pre-receive hook, I have commit="$(git hash-object -t tree '/dev/null')" instead.

P.S. I'm still looking for a good quality answer on my first question. By the way, I asked these questions on the official Git mailing list and received no answers so far, what a shame...

这篇关于Git'pre-receive'钩子和'git-clang-format'脚本可靠地拒绝违反代码风格惯例的推送的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆