Git 'pre-receive' 钩子和 'git-clang-format' 脚本可靠地拒绝违反代码样式约定的推送

Posted

技术标签:

【中文标题】Git \'pre-receive\' 钩子和 \'git-clang-format\' 脚本可靠地拒绝违反代码样式约定的推送【英文标题】:Git 'pre-receive' hook and 'git-clang-format' script to reliably reject pushes that violate code style conventionsGit 'pre-receive' 钩子和 'git-clang-format' 脚本可靠地拒绝违反代码样式约定的推送 【发布时间】:2016-02-28 17:10:55 【问题描述】:

让我们立即从我已经编写的 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

注意: 代码不相关的地方用# ... 存根。

注意: 如果你不熟悉git-clang-format,可以看看here。

该钩子按预期工作,到目前为止,我没有发现任何错误,但如果您发现任何问题或有改进建议,我将不胜感激任何报告。也许,我应该评论一下这个钩子背后的意图是什么。好吧,它确实使用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)。因此,您可以轻松查看角落案例所在的位置。是的,它与 root 提交(修订)有关。事实上,git reset --soft 'HEAD~1' 不能应用于根提交,因为它们没有要重置的父级。因此,对我的第二个问题进行以下检查:

        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 无法再处理它。我相信这与所有这些正在分析的推送修订(包括这个根)并不真正属于任何分支的事实有关。也就是说,它们处于分离HEAD 状态。为这种极端情况找到一个解决方案也是完美的,这样 initial 提交也可以通过git-clang-format 进行相同的检查,以符合代码样式约定。

和平。

【问题讨论】:

【参考方案1】:

注意: 对于那些正在寻找最新、(或多或少)全面且经过良好测试的解决方案的人,我托管了相应的公共存储库 [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 官方邮件列表上问了这些问题,至今没有收到任何答复,真可惜……

【讨论】:

【参考方案2】:

精简

我在理解第一个示例时遇到了一点麻烦,部分原因是长度和额外的花絮使其对 OP 的特定用例有用。我梳理了一下,浓缩成这样:

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

此示例使用 Prettier,但它可以很好地映射到 clang 格式、eslint 等。上面的示例(可能过于简单,但有效)有一些限制。我建议潜水更深...

更好,但更长

一旦您了解了这一点,我还建议您向下滚动到这个底部:

Reject Ugly Commits with Server-Side Git Hooks

【讨论】:

能否也将链接文本添加到您的消息中? @BrunoAlexandreRosa 在我看来,在这里重新发布完整的博客文章太长了。您认为这里有什么具体的内容吗?

以上是关于Git 'pre-receive' 钩子和 'git-clang-format' 脚本可靠地拒绝违反代码样式约定的推送的主要内容,如果未能解决你的问题,请参考以下文章

[转] 利用git钩子,使用python语言获取提交的文件列表

再谈git的http服务-权限控制hooks版

Git push提示pre-receive hook declined

在gitlab中新建项目,push的时候提示“pre-receive hook declined”

三分钟教你学Git(十七) - 钩子

Git 预推钩子