利用Gitlab钩子实现代码规范管控

1、前言

在一个开发团队中通常会碰到这样一个问题,那就是很多人的代码不够规范,导致可读性差甚至引发一些bug,然后就会有人出来制定代码规范了,制定完了开始推行,可是一段时间后却发现没人提了,于是代码又开始逐渐不够规范起来。那我们要怎么解决这个难题呢?那就需要引入一套强制规范要求的机制了,如果代码不够规范就不准提交到仓库里去,这样就不会让规范慢慢失去味道了,而是会一直强制执行下去。这就是本文将会提到的,利用Gitlab钩子来实现代码规范管控,将不合规范的代码挡之门外!

2、规范简介

我这里有三类规范,前端代码规范、后端代码规范、提交备注规范。本文提到的前端代码规范只包含vue和js代码的规范,参考的是Eslint-standard;后端代码规范只包含java代码规范,参考的是阿里代码规约;提交备注规范顾名而思义,参考:https://zhuanlan.zhihu.com/p/100773495

3、Gitlab钩子简介

Gitlab除了有大家熟悉的Webhook之外,还有可以自定义编码的钩子,这种钩子又分为两类:服务端钩子和客户端钩子。服务端钩子,顾名思义就是运行在Gitlab服务器上的钩子,是在代码提交到服务器之时执行的。而客户端钩子就是在本地仓库的钩子,是在本地提交代码的时候来执行的。他们各自又分为很多小类,这里需要使用到的是服务端的pre-receive钩子和客户端的commit-msg钩子。详细请参考官方文档:https://www.git-scm.com/book/zh/v2/%E8%87%AA%E5%AE%9A%E4%B9%89-Git-Git-%E9%92%A9%E5%AD%90

4、流程图

利用gitlab钩子实现代码规范管控的流程图

5、实施步骤

  1. 准备工作
    在gitlab服务器上安装jdk、nodejs。下载好阿里代码规范jar包p3c-pmd-2.0.0.jar,git地址:https://github.com/alibaba/p3c。git上只有源码,需要自己打成jar包,参考步骤:https://www.jianshu.com/p/b87ca8615c9c,打包比较麻烦,需要注意build.gradle里面的参数,也可以下载我做好的jar包,地址:p3c-pmd-2.0.0.jar
    附上gradle.build的配置文档:

    /*
     * This file was generated by the Gradle 'init' task.
     */
    
    plugins {
        id 'java'
        id 'maven-publish'
    }
    
    repositories {
        mavenLocal()
        maven {
            url = uri('https://oss.sonatype.org/content/repositories/snapshots')
        }
    
        maven {
            url = uri('https://repo.maven.apache.org/maven2/')
        }
    }
    
    dependencies {
        implementation 'net.sourceforge.pmd:pmd-java:6.15.0'
        implementation 'net.sourceforge.pmd:pmd-vm:6.15.0'
        implementation 'javax.annotation:javax.annotation-api:1.3.2'
        testImplementation 'net.sourceforge.pmd:pmd-test:6.15.0'
    }
    
    group = 'com.alibaba.p3c'
    version = '2.0.0'
    description = 'p3c-pmd'
    java.sourceCompatibility = JavaVersion.VERSION_1_8
    
    java {
        withJavadocJar()
    }
    
    publishing {
        publications {
            maven(MavenPublication) {
                from(components.java)
            }
        }
    }
    
    tasks.withType(JavaCompile) {
        options.encoding = 'UTF-8'
    }
    
    jar {
        from {
            configurations.runtime.collect{zipTree(it)}
        }
    }
    
  2. 客户端钩子
    脚本如下,需要拷贝到本地代码目录的.git/hook/commit-msg.sample,并且去掉.sample后缀,钩子就可以执行了。

    #!/bin/sh
    #
    # An example hook script to check the commit log message.
    # Called by "git commit" with one argument, the name of the file
    # that has the commit message.  The hook should exit with non-zero
    # status after issuing an appropriate message if it wants to stop the
    # commit.  The hook is allowed to edit the commit message file.
    #
    # To enable this hook, rename this file to "commit-msg".
    
    # Uncomment the below to add a Signed-off-by line to the message.
    # Doing this in a hook is a bad idea in general, but the prepare-commit-msg
    # hook is more suited to it.
    #
    # SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p')
    # grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1"
    
    # This example catches duplicate Signed-off-by lines.
    
    # @author amzheng@deloitte.com.cn
    # @date 2021-04-09
    # @description 在提交时预检查备注信息是否符合规范,如不符合规范会被退回
    
    echo 'commit-msg hook running...'
    
    msg_file=$1
    
    msg=`cat ${msg_file} | grep -v "^$"`
    
    TYPE_LIST=(
         'feat:'   #新功能feature
         'update:' #在feat内修改
         'fix:'  #修补bug
         'docs:'  #文档
         'style:' #格式化,不影响代码运行的变动
         'refactor:' #重构
         'pref:'  #性能优化
         'test:'  #增加测试
         'chore:'  #构建过程或辅助工具的变动
         #'[ci skip]'  #忽略校验
    )
    COMMIT_MESSAGE_MIN_LENGTH=10
    
    declare -a regex_list
    arrLen=${#TYPE_LIST[@]}
    for ((i=0;i<$arrLen;i++)) do
    	regex_list[i]='^'${TYPE_LIST[i]}
    done
    regex_list[$arrLen+1]='^[ci skip]:'
    #echo "reg_list=== "${regex_list[@]}
    separator="|"
    ## 合并成一个完整的正则表达式
    regex="$( printf "${separator}%s" "${regex_list[@]}" )"
    #echo "type regex: "$regex
    ## 去除头部的 |
    regex=${regex:${#separator}}
    #echo "regex: "$regex
    
    tips_msg="$( printf "${separator}%s" "${TYPE_LIST[@]}" )"
    tips_msg=${tips_msg:${#separator}}
    
    echo 'Start validate commit comment:'$msg
    match=`echo $msg | grep -nE "(${regex})"`
    #echo 'Match result: '$match
    
    ## 找到匹配说明是符合规范的
    if [ "${match}" != "" ]; then
    	## 校验注释长度
    	msg_length=${#msg}
    	#echo "Msg length: ${msg_length}"
    	if [[ ${msg_length} -lt ${COMMIT_MESSAGE_MIN_LENGTH} ]]; then
    		echo -e "pre-commit Error: Commit message should be bigger than ${COMMIT_MESSAGE_MIN_LENGTH} and current commit message length: ${msg_length}"
    		exit 1
    	fi
    
    	## 其他操作
    	echo "commit-msg: Commit comments validate Success!"
    else
    	echo -e "commit-msg Error: Commit comments message should be started with [${tips_msg}]..."
    	exit 1
    fi
    
  3. 服务端钩子
    脚本如下。需要把代码拷贝到:/opt/gitlab/embedded/service/gitlab-shell/hooks目录下的pre-receive钩子中,这个目录是每个代码仓库共享的,相当于添加的是公共钩子,每个代码库的代码都会检测,如果需要只添加到某个库,可以把脚本放到仓库目录: /var/opt/gitlab/git-data/repositories/xxx/xxx.git/custom_hooks。

    #!/bin/bash
    
    ##脚本提供功能:Commit提交的Message和代码规范是否符合统一规范
    ##分三个部分:
    # 1.变量定义部分
    # 2.校验部分:注释校验&代码分析
    # 3.初始化入口
    ## 校验流程:
    # 1.先做提交注释校验,校验的规则:是否已${TYPE_LIST}定义的开头,且内容长度是否大于	${COMMIT_MESSAGE_MIN_LENGTH}
    # 2.如果是master分之,修改了pom文件还会校验是否存在snapshot版本的jar
    # 3.最后代码规范校验
    ## (单个项目校验)文件放置目录
    # 1./var/opt/gitlab/git-data/repositories/@hashed/xx/xx/xx.git或者/var/opt/gitlab/git-data/repositories/${group}/${project_name}.git/
    # 2.创建custom_hooks目录
    # 3.在custom_hooks目录下创建pre-receive文件,并保持776可执行权限,且保持该文件权限:chown git:git pre-receive 以及阿里云的p3c-pmd的jar包权限
    # 4.给chown -R git:git custom_hooks
    # 5.官方文档说明:https://docs.gitlab.com/ee/administration/custom_hooks.html#setup
    
    # @author amzheng@deloitte.com.cn
    # @date 2021-04-09
    # @description 在服务端接收提交文件时检查备注规范以及代码规范,如不符合规范会被退回
    
    
    ####### 初始化变量部分 #########
    
    ## 定义java_home变量 需要修改你配置的java_home
    JAVA_HOME=/usr/local/jdk/jdk1.8.0_191
    ## 是否开启commit message的校验:0是,1否
    CHECK_COMMIT_MESSAGE_ON=0
    ## 是否开启代码检查:0是,1否
    CHECK_CODE_RULE_ON=0
    ## 是否校验master上的pom文件是否包含snapshot:0是,1否
    CHECK_MASTER_POM_SNAPSHOT_ON=1
    ## 注释内容最小长度,默认20
    COMMIT_MESSAGE_MIN_LENGTH=20
    ### 代码校验规则:0使用阿里云P3C规则,1使用checkStyle
    CODE_RULE_TYPE=0
    
    ## 定义提交开头类型字符规则
    ## e.g: fix:测试提交bug修复,Bug编号#12
    TYPE_LIST=(
         'feat:'   #新功能feature
         'update:' #在feat内修改
         'fix:'  #修补bug
         'docs:'  #文档
         'style:' #格式化,不影响代码运行的变动
         'refactor:' #重构
         'pref:'  #性能优化
         'test:'  #增加测试
         'chore:'  #构建过程或辅助工具的变动
         #'[ci skip]'  #忽略校验
    )
    
    ## 获取当前路径
    BASE_PATH=$(cd `dirname $0`; pwd)
    #echo 'BASE_PATH: '$BASE_PATH
    
    #定义和组装校验规则
    declare -a regex_list
    arrLen=${#TYPE_LIST[@]}
    for ((i=0;i<$arrLen;i++)) do
    	regex_list[i]='^'${TYPE_LIST[i]}
    done
    regex_list[$arrLen+1]='^[ci skip]:'
    #echo "reg_list=== "${regex_list[@]}
    separator="|"
    ## 合并成一个完整的正则表达式
    regex="$( printf "${separator}%s" "${regex_list[@]}" )"
    #echo "type regex: "$regex
    ## 去除头部的 |
    regex=${regex:${#separator}}
    #echo "regex: "$regex
    
    ## 定义注释出错提示信息
    tips_msg="$( printf "${separator}%s" "${TYPE_LIST[@]}" )"
    tips_msg=${tips_msg:${#separator}}
    ####### 初始化变量部分 #########
    
    ####### 校验部分:注释校验&代码分析###########
    ## 校验commit message
    validate_commit_message()
    {
    	 oldrev=$(git rev-parse $1)
    	newrev=$(git rev-parse $2)
    	refname="$3"
    	#echo 'Old version: '$oldrev
    	#echo 'New version: '$newrev
    	#echo 'Branch: '$refname
    
    	## git 命令
    	#GITCMD="git"
    	## 按时间倒序列出 commit  找出两个版本之间差异的版本号集合  oldrev~newrev
    	commitList=`git rev-list $oldrev..$newrev`
    	#echo 'commitList: '$commitList
    
    	split=($commitList)
    	#echo 'split: '$split
    
     	# 遍历数组
    	for s in ${split[@]}
    	do
      		#echo “$s”
      		#通过版本号获取仓库中对象实体的类型、大小和内容的信息
          #比如提交人、作者、邮件、提交时间、提交内容等
          currentContent=`git cat-file commit $s`
          #echo 'Commit obj: '$currentContent
          #获取提交内容
          msg=`git cat-file commit $s | sed '1,/^$/d'`
          echo 'msg: '$msg
    
        	## merge合并分之直接放行
       		 if [[ $msg == *"Merge branch"* ]]; then
        		echo "Merge branch...skip the checking"
        	else
    		    ## 做内容校验
    		    match=`echo $msg | grep -nE "(${regex})"`
    		    #echo 'Match result: '$match
    
    	    	## 找到匹配说明是符合规范的
    		    if [ "${match}" != "" ]; then
          			## 校验注释长度
          			msg_length=${#msg}
          			#echo "Msg length: ${msg_length}"
          		if [[ ${msg_length} -lt ${COMMIT_MESSAGE_MIN_LENGTH} ]]; then
            		echo -e "Error: Commit message should be bigger than ${COMMIT_MESSAGE_MIN_LENGTH} and current commit message length: ${msg_length}"
            		exit 1
         		fi
    
          ### 找到匹配内容做相应处理,如fix ,校验pom文件等
          #if [[ "${match}" =~ "fix:" ]]; then
            ## 如果是修补bug,规范有点获取到fix中的ID,然后调用禅道对外的API关闭,其他场景类似
          #fi
    
          # 是否开启校验和master分之
          isMaster=$(echo $refname | grep "master$")
          if [ $CHECK_MASTER_POM_SNAPSHOT_ON == 0 ] && [ -n "$isMaster" ]; then
            # 如果是master分之,并且pom文件发生了变更,判断pom文件是否含有sonapshot的引用
            pomfile=`git diff --name-only ${oldrev} ${newrev} | grep -e "pom\.xml"`
            if [[ "${pomfile}" != "" ]]; then
              #echo $pomfile
              ## 获取pom文件更新的内容
              pomcontent=`git show $newrev:$pomfile`
              #echo $pomcontent
              ## 校验pom文件是否包含snapshot版本
              if [[ $pomcontent =~ 'SNAPSHOT' ]]; then
                echo -e "Error: Snapshot version cannot exist in master branch!"
                exit 1
              fi
            fi
          fi
    
          ## 其他操作
          echo "Commit comments validate Success!"
        else
          echo -e "Error: Commit comments message should be started with [${tips_msg}]..."
          exit 1
        fi
    	  fi
    done
    }
    
    ## 代码校验
    validate_code_rules()
    {
    echo 'Start code analysis!'
    oldrev=$(git rev-parse $1)
    newrev=$(git rev-parse $2)
    refname="$3"
    #echo 'Old version: '$oldrev
    #echo 'New version: '$newrev
    #echo 'Branch: '$refname
    
    TEMPDIR=$BASE_PATH/"tmp"
    
    FILES=`git diff --name-only ${oldrev} ${newrev}  | grep -e "\.java$"`
    
    if [ -n "$FILES" ]; then
      for FILE in ${FILES}; do
          mkdir -p "${TEMPDIR}/`dirname ${FILE}`" >/dev/null
          git show $newrev:$FILE > ${TEMPDIR}/${FILE}
      done;
    
      MAIN_JAVA_PATH=$TEMPDIR'/src/main'
      #echo 'Temp update files path: '$MAIN_JAVA_PATH
    
      #FILES_TO_CHECK=`find $MAIN_JAVA_PATH -name '*.java'`
    
      #echo 'Check files:'${FILES_TO_CHECK}
      echo 'Aliyun p3c-pmd check starting.....'
    
      #echo 'Current shell Path:' $BASE_PATH
      #echo 'JAVA_HOME:' $JAVA_HOME
      #echo 'Root directory for java sources: '$MAIN_JAVA_PATH
    
      if [[ $CODE_RULE_TYPE == 0 ]]; then
         ## 需要阿里云P3C的插件包p3c-pmd-2.0.0.jar与该脚本在同级目录下
         echo 'Code analysis for Aliyun-p3c..'
         #$JAVA_HOME/bin/java -Dpmd.language=en -cp $BASE_PATH/p3c-pmd-2.0.0.jar net.sourceforge.pmd.PMD -d $MAIN_JAVA_PATH -R rulesets/java/ali-comment.xml,rulesets/java/ali-concurrent.xml,rulesets/java/ali-constant.xml,rulesets/java/ali-exception.xml,rulesets/java/ali-flowcontrol.xml,rulesets/java/ali-naming.xml,rulesets/java/ali-oop.xml,rulesets/java/ali-orm.xml,rulesets/java/ali-other.xml,rulesets/java/ali-set.xml -f text
         $JAVA_HOME/bin/java -Dpmd.language=en -cp $BASE_PATH/p3c-pmd-2.0.0.jar net.sourceforge.pmd.PMD -d $TEMPDIR -R rulesets/java/ali-comment.xml,rulesets/java/ali-concurrent.xml,rulesets/java/ali-constant.xml,rulesets/java/ali-exception.xml,rulesets/java/ali-flowcontrol.xml,rulesets/java/ali-naming.xml,rulesets/java/ali-oop.xml,rulesets/java/ali-other.xml,rulesets/java/ali-set.xml -f text
         RESULT=$?
         #echo $RESULT
         if [ $RESULT -gt 0 ]; then
    		rm -rf $TEMPDIR
    		exit 1;
         fi
      elif [[ $CODE_RULE_TYPE == 1 ]]; then
         ## 需要CheckStyle插件包checkstyle-8.16-all与该脚本在同级目录下,并且需要对应的CheckStyle.xml模板文件e.g:Cheetah_Checkstyle_ruleset.xml
         echo 'Code analysis for CheckStyle..'
         CHECK_RESULT=`$JAVA_HOME/bin/java -jar $BASE_PATH/checkstyle-8.16-all.jar -c $BASE_PATH/Cheetah_Checkstyle_ruleset.xml $MAIN_JAVA_PATH`
     echo 'Check_style check result:'
     #echo $CHECK_RESULT 
       if [[ $CHECK_RESULT =~ "[WARN]" ]]; then
         echo $CHECK_RESULT | sed 's/\[WARN\]/\n/g'
    	 rm -rf $TEMPDIR
         exit 1
       fi
      else
         ## 不支持的检查操作
         echo "Unsupported code validation rule,Please contact the administrator to check the configuration of [CODE_RULE_TYPE] in pre-receive script!"
    	 rm -rf $TEMPDIR
         exit 1
      fi
    
      echo 'Code analysis success!'
      
    else
    	echo 'No java code, analysis end!'
    fi
    
    FRONT_FILES=`git diff --name-only ${oldrev} ${newrev}  | grep -e "\.vue\|\.js$"`
    
    echo 'Start analysis vue & js code...'
    if [ -n "$FRONT_FILES" ];then
    	PASS=true
    
    	for FILE in $FRONT_FILES; do
    		mkdir -p "${TEMPDIR}/`dirname ${FILE}`" >/dev/null
    		git show $newrev:$FILE > ${TEMPDIR}/${FILE}
    	done;
    	
    	eslint $TEMPDIR/**/*.vue
    	eslint $TEMPDIR/**/*.js
    	check_result=`eslint $TEMPDIR/**/*.vue`
    	if [ ${#check_result[*]} -gt 0 ]; then
    		echo "vue & js code analysis failed!"
    		rm -rf $TEMPDIR
    		exit 1
    	fi
    	
    	check_result=`eslint $TEMPDIR/**/*.js`
    	if [ ${#check_result[*]} -gt 0 ]; then
    		echo "vue & js code analysis failed!"
    		rm -rf $TEMPDIR
    		exit 1
    	fi
    
    	echo "vue & js code analysis success!"
    else
    	echo 'No vue & js code, analysis end!'
    fi
    
    rm -rf $TEMPDIR
    }
    ####### 校验部分:注释校验&代码分析###########
    
    ####### 执行入口###########
    pre_receive()
    {
    #commit message 校验
    if [[ $CHECK_COMMIT_MESSAGE_ON == 0 ]]; then
       validate_commit_message $1 $2 $3
    fi
    
    #代码规则检查
    if [[ $CHECK_CODE_RULE_ON == 0 ]]; then
       validate_code_rules $1 $2 $3
    fi
    }
    
    # update hook触发会带参数执行if逻辑
    # hooks脚本触发无参数执行else逻辑
    if [ -n "$1" -a -n "$2" -a -n "$3" ]; then
    	# Output to the terminal in command line mode - if someone wanted to
    	# resend an email; they could redirect the output to sendmail
    	# themselves
    	pre_receive $2 $3 $1
    	#echo $1'+'$2'+'$3
    else
    while read oldrev newrev refname
    do
       pre_receive $oldrev $newrev $refname
       #echo $oldrev' '$newrev' '$refname
    done
    fi
    ####### 执行入口###########
    exit 0
    
  4. Eslint配置文件:.eslintrc.js

    module.exports = {
    root: true,
    parserOptions: {
        parser: 'babel-eslint'
      },
      env: {
        browser: true,
      },
      extends: [
        // https://github.com/vuejs/eslint-plugin-vue#priority-a-essential-error-prevention
        // consider switching to `plugin:vue/strongly-recommended` or `plugin:vue/recommended` for stricter rules.
        'plugin:vue/essential', 
        // https://github.com/standard/standard/blob/master/docs/RULES-en.md
        'standard'
      ],
      // required to lint *.vue files
      plugins: [
        'html'
      ],
      // add your custom rules here
      rules: {
        // allow async-await
        'generator-star-spacing': 'off',
        // allow debugger during development
        'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
    	'no-console': 2,
        // js语句结尾必须使用 ;
        // 'semi': ['off', 'always'],
        'semi': ['error', 'always'],
        // 三等号
        'eqeqeq': 0,
        // 强制在注释中 // 或 /* 使用一致的空格
        'spaced-comment': 0,
        // 关键字后面使用一致的空格
        'keyword-spacing': 0,
        // 强制在 function的左括号之前使用一致的空格
        'space-before-function-paren': 0,
        // 引号类型
        "quotes": [0, "single"],
        "vue/no-parsing-error": [2, { "x-invalid-end-tag": false }]
      }
    }
    
  5. 拷贝完后需要执行以下命令:

    /opt/gitlab/embedded/service/gitlab-shell/hooks
    目录下添加pre-receive、p3c-pmd-2.0.0.jar、.eslintrc.js,并赋予执行权限:chmode –R 777/opt/gitlab/embedded/service/gitlab-shell/hooks目录下执行如下命令:
    npm install -g eslint-plugin-vue
    npm install eslint-plugin-vue@latest --save-dev
    npm install vue@latest --save-dev
    npm install eslint@latest --save-dev
    npm install eslint-config-standard@latest --save-dev
    npm install eslint-plugin-html@latest --save-dev
    npm install eslint-plugin-import@latest --save-dev
    npm install eslint-plugin-node@latest --save-dev
    npm install eslint-plugin-promise@latest --save-dev
    npm install eslint babel-eslint --save-dev
    
    在/opt/gitlab/embedded/service/gitlab-shell/hooks目录下新建临时目录tmp,执行如下命令:
    chown git:git ./hooks
    chown git:git tmp/
    chown git:git tmp/* 
    
  6. 大公告成,可以测试一下看看了。

  • 0
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值