之前写过一篇 SVN服务端集成Checkstyle实现代码自动静态检查。最近部门打算试点GIT作为SCM,因此需要将相应的服务端检测功能进行一次迁移,于是就有了本篇博文。

1. 概述

对于Git通过Hook实现静态代码检测,大致分为两个方向:

  1. 借助Client-Side-Hook来实现。此方法对应于研发人员工作机上的${PROJECT_ROOT}/.git/hooks/pre-commit脚本实现。
  2. 借助Server-Side-Hook来实现。此方法对应于Git服务端的/var/opt/gitlab/git-data/repositories/<group>/<project>.git/custom_hooks/pre-receive脚本。

相比较之下,两种实现方式之中,参考资料丰富度层面前一种胜出,但笔者所在部门希望尽可能地促使研发人员将问题原地解决,因此我们最终决定求助于第二种方案 —— Server-Side-Hook。

2. Server-Side-Hook

关于Server-Side-HookGitLab - Server_Hooks已经有了详尽的介绍,这里我们不出意外地将目光集中到了 pre-receive

2.1 准备检测工具

准备CheckStyle/PMD相关JAR和XML规则文件。

  1. CheckStyle - JAR
  2. PMD - JAR

将下载的JAR和对应的XML推送到服务器对应位置,笔者这里是直接放到了对应项目根目录下:

  1. /var/opt/gitlab/git-data/repositories/<group>/<project>.git/checkstyle
  2. /var/opt/gitlab/git-data/repositories/<group>/<project>.git/pmd-bin-6.20.0
2.2 创建pre-receive脚本文件

首先我们需要在对应位置创建一个空白的pre-receive文件(注意该文件没有后缀)。

# 切换到服务端项目目录下
cd /var/opt/gitlab/git-data/repositories/<group>/<project>.git
mkdir custom_hooks
cd custom_hooks
vi pre-receive

接下来就是重头戏。

2.3 pre-receive脚本内容

注意这里的脚本实现方式相当粗暴,因为笔者打算借鉴自其它产品来对此类功能做一次封装,因此这里的要求只是生效即可。

#!/usr/bin/env bash

# From 

#加载环境变量, 让下面的java指令生效
source /etc/profile

# 创建临时目录
TMP_DIR=$(mktemp -d)

# CheckStyle
CHECKSTYLE_BASE_PATH="/var/opt/gitlab/git-data/repositories/<group>/<project>.git/checkstyle"
CHECKSTYLE_JAR_FILE="$CHECKSTYLE_BASE_PATH/checkstyle-8.17-all.jar"
CHECKSTYLE_XML_FILE="$CHECKSTYLE_BASE_PATH/checkstyle-OLD.xml"

# PMD
PMD_BASE_PATH="/var/opt/gitlab/git-data/repositories/<group>/<project>.git/pmd-bin-6.20.0"
PMD_CLASSPATH="$PMD_BASE_PATH/lib/*"
PMD_XML_FILE="$PMD_BASE_PATH/kanq-pmd-bestpratice-OLD.xml"

# 错误数
errors_count=0
 
# 空hash
EMPTY_REF='0000000000000000000000000000000000000000'
 
while read oldrev newrev ref 
do
    # 当push新分支的时候oldrev会不存在,删除时newrev就不存在
    if [[ $oldrev != $EMPTY_REF && $newrev != $EMPTY_REF ]]; then
        # 被检查了的文件数
        checked_file_count=0
        # 找出哪些文件被更新了
        for line in $(git diff-tree -r $oldrev..$newrev | grep -oP '.*\.(java)' | awk '{print $5$6}')
        do
            # 文件状态. 本地查询命令: git status --help
            # D: deleted
            # A: added
            # M: modified
            status=$(echo $line | grep -o '^.')
            # 不检查被删除的文件
            if [[ $status == 'D' ]]; then
                continue
            fi
 
            # 文件名
            file=$(echo $line | sed 's/^.//')
            # 为文件创建目录
            mkdir -p $(dirname $TMP_DIR/$file)
            # 保存文件内容
            git show $newrev:$file > $TMP_DIR/$file
            output=$(java -jar $CHECKSTYLE_JAR_FILE -c $CHECKSTYLE_XML_FILE  $TMP_DIR/$file)
            outputPMD=$(java -classpath "$PMD_CLASSPATH" net.sourceforge.pmd.PMD -R $PMD_XML_FILE -dir $TMP_DIR)            
            # 以下命名管道是为了将形如"Checkstyle ends with 89 errors."字符串中的 89 找出来
            error=$(echo $output | grep -oP '([0-9]+) errors' | grep -oP '[0-9]+')
            errorPMD=$(echo $outputPMD | grep -oP '[0-9]{2,}' | wc -l)
           
            if [[ $errorPMD || $error ]]; then
 
                msg="${file}: "
 
                if [[ $errorPMD > 0 ]]; then
                    let "warnings_count = warnings_count + 1"
                    echo $outputPMD
                    rm -rf $TMP_DIR
                    exit 1
                fi
                if [[ $error > 0 ]]; then
                    msg="$msg has checkstyle [${error}] errors"
                    echo $output
                    rm -rf $TMP_DIR
                    exit 1  
                    let "errors_count = errors_count + 1"
                fi
 
                echo -e "    $msg"
            fi
 
            let "checked_file_count = checked_file_count + 1";
 
        done
 
       if [[ $checked_file_count == 0 ]]; then
           echo -e " No file was checked."
       elif [[ $warnings_count == 0 && $errors_count == 0 ]]; then
           echo -e "'Congratulations!!! no error, no warning'"
       elif [[ $errors_count  == 0 ]]; then
           echo -e " Good job, no errors were found!!!"
       fi
         
    fi
done
 
# 删除临时目录
rm -rf $TMP_DIR

if [[ $warnings_count > 0 || $errors_count > 0 ]]; then
    echo 'check fail. please check yourself local'
    exit 1
fi


exit 0

3. 效果

Java 检测 gitlab健康 gitlab checkstyle_ide


错误提示不如 SVN集成PMD实现代码自动静态检查 有条理,但有个消息是"错误提示的行数正常了"。

4. 建议

  1. 可以考虑使用global server hook,然后针对特例再作 single repository server hook。
  2. 鉴于SVN和GIT设计思路的不同,在使用GIT时候如果出现检测失败,修复之后一定要记得再commit一次,然后再次push。其它方式还有建立Client Site Hook - pre commit 以及 取消本地commit。

5. Links

  1. SVN集成Checkstyle实现代码自动静态检查
  2. Atlassian - Bitbucket - Server-Side-Hooks 笔者个人认为相较于下面的文档,Atlassian文档至少在这一点上更为丰富。
  3. GitLab - Server_Hooks
  4. Git - Server-Side-Hooks
  5. Git 使用 pre commit 实现代码自动静态检查 //需要BASH支持
  6. CheckStyle Git hook 简单配置 // 需要Python支持