一.Hooks检查大致流程
- 编写CheckStyle文件: 检查JavaCode的规范文件
- 编写Gradle:执行checkstyle task方法并调用checkstyle.xml对修改过的java文件进行检查
- 编写pre-commit脚本文件:在app目录下执行gradlew命令中的checkstyle task
- 编写commit-msg脚本文件:对代码提交所输入的日志进行规范检查
- 编写hooks脚本文件:自动在.git下生成pre-commit&commit-msg
二.Hooks检查代码
1. CheckStyle.xml文件:
如下所示一个比较完整的demo,检查的规范比较多,可以根据公司or项目组情况自定义规范。
注意几点:
a.配置文件中不能有重复的规则,否则创建或者导入时可能会导致失败。
b.配置文件中每一个<module> </module> 代表一个检测的功能点。
c.配置文件中的< property > </property > 可以配置一些参数,其中name="severity"参数表示该代码规范报错的严重程度,如果严重程度设置为error,则提交会不通过。
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE module PUBLIC
"-//Puppy Crawl//DTD Check Configuration 1.3//EN"
"http://www.puppycrawl.com/dtds/configuration_1_3.dtd">
<module name="Checker">
<property name="charset" value="UTF-8" />
<property name="severity" value="warning" />
<!-- 检查文件是否以一个空行结束 -->
<module name="NewlineAtEndOfFile" />
<!-- 文件长度不超过1500行 -->
<module name="FileLength">
<property name="max" value="1500" />
</module>
<!-- 每个java文件一个语法树 -->
<module name="TreeWalker">
<!-- import检查-->
<!-- 避免使用* -->
<module name="AvoidStarImport">
<property name="excludes" value="java.io,java.net,java.lang.Math" />
<!-- 实例;import java.util.*;.-->
<property name="allowClassImports" value="false" />
<!-- 实例 ;import static org.junit.Assert.*;-->
<property name="allowStaticMemberImports" value="true" />
</module>
<!-- 检查是否从非法的包中导入了类 -->
<module name="IllegalImport" />
<!-- 检查是否导入了多余的包 -->
<module name="RedundantImport" />
<!-- 没用的import检查,比如:1.没有被用到2.重复的3.import java.lang的4.import 与该类在同一个package的 -->
<module name="UnusedImports" />
<!-- 注释检查 -->
<!-- 检查方法和构造函数的javadoc -->
<module name="JavadocType">
<property name="allowUnknownTags" value="true" />
<message key="javadoc.missing" value="Class Comments: Lack of Javadoc annotations." />
</module>
<module name="JavadocMethod">
<property name="tokens" value="METHOD_DEF" />
<!--允许get set 方法没有注释-->
<property name="allowMissingPropertyJavadoc" value="true" />
<message key="javadoc.missing" value="Method Comments: Lack of Javadoc annotations." />
</module>
<!-- 命名检查 -->
<!-- 局部的final变量,包括catch中的参数的检查 -->
<module name="LocalFinalVariableName" />
<!-- 局部的非final型的变量,包括catch中的参数的检查 -->
<module name="LocalVariableName" />
<!-- 包名的检查(只允许小写字母),默认^[a-z]+(\.[a-zA-Z_][a-zA-Z_0-9_]*)*$ -->
<module name="PackageName">
<!--<property name="severity" value="error" />-->
<!--<property name="format" value="^[a-z]+(\.[a-z][a-z0-9]*)*$" />-->
<!--<message key="name.invalidPattern"-->
<!--value="Class comment package names are allowed only in lowercase letters." />-->
</module>
<!-- 仅仅是static型的变量(不包括static final型)的检查 -->
<module name="StaticVariableName" />
<!-- Class或Interface名检查,默认^[A-Z][a-zA-Z0-9]*$-->
<module name="TypeName">
<property name="severity" value="warning" />
<message key="name.invalidPattern"
value="Class comment package names are allowed only in lowercase letters." />
</module>
<!-- 非static型变量的检查 -->
<module name="MemberName" />
<!-- 方法名的检查 -->
<module name="MethodName" />
<!-- 方法的参数名 -->
<module name="ParameterName " />
<!-- 常量名的检查(只允许大写),默认^[A-Z][A-Z0-9]*(_[A-Z0-9]+)*$ -->
<module name="ConstantName" />
<!-- 定义检查 -->
<!-- 检查数组类型定义的样式 -->
<module name="ArrayTypeStyle" />
<!-- 检查long型定义是否有大写的“L” -->
<module name="UpperEll" />
<!-- 长度检查 -->
<!-- 每行不超过120个字符 -->
<module name="LineLength">
<property name="max" value="120" />
</module>
<!-- 方法不超过50行 -->
<module name="MethodLength">
<property name="tokens" value="METHOD_DEF" />
<property name="max" value="50" />
</module>
<!-- 方法的参数个数不超过5个。 并且不对构造方法进行检查-->
<module name="ParameterNumber">
<property name="max" value="5" />
<property name="ignoreOverriddenMethods" value="true" />
<property name="tokens" value="METHOD_DEF" />
<property name="severity" value="error"/>
</module>
<!-- 空格检查-->
<!-- 方法名后跟左圆括号"(" -->
<module name="MethodParamPad" />
<!-- 在类型转换时,不允许左圆括号右边有空格,也不允许与右圆括号左边有空格 -->
<module name="TypecastParenPad" />
<!-- 检查在某个特定关键字之后应保留空格 -->
<module name="NoWhitespaceAfter" />
<!-- 检查在某个特定关键字之前应保留空格 -->
<module name="NoWhitespaceBefore" />
<!-- 操作符换行策略检查 -->
<module name="OperatorWrap" />
<!-- 圆括号空白 -->
<module name="ParenPad" />
<!-- 检查分隔符是否在空白之后 -->
<module name="WhitespaceAfter" />
<!-- 检查分隔符周围是否有空白 -->
<module name="WhitespaceAround" />
<!-- 修饰符检查 -->
<!-- 检查修饰符的顺序是否遵照java语言规范,默认public、protected、private、abstract、static、final、transient、volatile、synchronized、native、strictfp -->
<module name="ModifierOrder" />
<!-- 检查接口和annotation中是否有多余修饰符,如接口方法不必使用public -->
<module name="RedundantModifier" />
<!-- 代码块检查 -->
<!-- 检查是否有嵌套代码块 -->
<module name="AvoidNestedBlocks" />
<!-- 检查是否有空代码块 -->
<module name="EmptyBlock" />
<!-- 检查左大括号位置 -->
<module name="LeftCurly" />
<!-- 检查代码块是否缺失{} -->
<module name="NeedBraces" />
<!-- 检查右大括号位置 -->
<module name="RightCurly" />
<!-- 代码检查 -->
<!-- 检查空的代码段 -->
<module name="EmptyStatement" />
<!-- 检查在重写了equals方法后是否重写了hashCode方法 -->
<module name="EqualsHashCode" />
<!-- 检查局部变量或参数是否隐藏了类中的变量 -->
<module name="HiddenField">
<property name="tokens" value="VARIABLE_DEF" />
</module>
<!-- 检查是否使用工厂方法实例化 -->
<module name="IllegalInstantiation" />
<!-- 检查子表达式中是否有赋值操作 -->
<module name="InnerAssignment" />
<!-- 检查是否有"魔术"数字 -->
<module name="MagicNumber">
<property name="ignoreNumbers" value="0, 1" />
<property name="ignoreAnnotation" value="true" />
</module>
<!-- 检查switch语句是否有default -->
<module name="MissingSwitchDefault" />
<!-- 检查是否有过度复杂的布尔表达式 -->
<module name="SimplifyBooleanExpression" />
<!-- 检查是否有过于复杂的布尔返回代码段 -->
<module name="SimplifyBooleanReturn" />
<!-- 类设计检查 -->
<!-- 检查类是否为扩展设计l -->
<!-- 检查只有private构造函数的类是否声明为final -->
<module name="FinalClass" />
<!-- 检查工具类是否有putblic的构造器 -->
<module name="HideUtilityClassConstructor" />
<!-- 检查接口是否仅定义类型 -->
<module name="InterfaceIsType" />
<!-- 检查类成员的可见度 检查类成员的可见性。只有static final 成员是public的
除非在本检查的protectedAllowed和packagedAllowed属性中进行了设置-->
<module name="VisibilityModifier">
<property name="packageAllowed" value="true" />
<property name="protectedAllowed" value="true" />
</module>
<!-- 语法 -->
<!-- String的比较不能用!= 和 == -->
<module name="StringLiteralEquality" />
<!-- 限制for循环最多嵌套2层 -->
<module name="NestedForDepth">
<property name="max" value="2" />
</module>
<!-- if最多嵌套3层 -->
<module name="NestedIfDepth">
<property name="max" value="3" />
</module>
<!-- 检查未被注释的main方法,排除以Appllication结尾命名的类 -->
<module name="UncommentedMain">
<property name="excludedClasses" value=".*Application$" />
</module>
<!-- 禁止使用System.out.println -->
<module name="Regexp">
<property name="format" value="System\.out\.println" />
<property name="illegalPattern" value="true" />
</module>
<!-- return个数 3个-->
<module name="ReturnCount">
<property name="max" value="3" />
</module>
<!--try catch 异常处理数量 3-->
<module name="NestedTryDepth ">
<property name="max" value="3" />
</module>
<!-- clone方法必须调用了super.clone() -->
<module name="SuperClone" />
<!-- finalize 必须调用了super.finalize() -->
<module name="SuperFinalize" />
</module>
</module>
2. Gradle文件:
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.4.1'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
google()
jcenter()
}
apply plugin: 'checkstyle'
checkstyle {
toolVersion '6.13'
ignoreFailures false
showViolations true
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
// 脚本pre-commit 调用 checkstyle task 检查代码(修改提交的代码)
task checkstyle(type: Checkstyle) {
source 'app/src/main/java'
exclude '**/gen/**'
exclude '**/R.java'
exclude '**/BuildConfig.java'
if (project.hasProperty('checkCommit') && project.property("checkCommit")) {
List<String> fileS = filterCommitter(getChangeFiles())
def includeList = new ArrayList<String>()
for (String split : fileS) {
String[] splits = split.split("/")
String fileName = splits[splits.length - 1]
includeList.add("**/" + fileName)
}
if (includeList.size() == 0) {
exclude '**/*.java'
} else {
println("includeList==" + includeList)
include includeList
}
} else {
include '**/*.java'
}
configFile rootProject.file('hook/checkstyle.xml')
classpath = files()
}
//根据git status 获取 修改的文件
def getChangeFiles() {
try {
String changeInfo = 'git status -s'.execute(null, project.rootDir).text.trim()
return changeInfo == null ? "" : changeInfo
} catch (Exception e) {
return ""
}
}
//只对java代码
def filterCommitter(String info) {
ArrayList<String> filterList = new ArrayList<String>()
String[] lines = info.split("\\n")
for (String line : lines) {
if (line.contains(".java")) {
String[] split = line.trim().split(" ")
for (String str : split) {
if (str.contains(".java")) {
filterList.add(str)
}
}
}
}
return filterList
}
3. pre-commit脚本代码:
#!/bin/sh
#
# An example hook script to verify what is about to be committed.
# Called by "git commit" with no arguments. The hook should
# exit with non-zero status after issuing an appropriate message if
# it wants to stop the commit.
#
# To enable this hook, rename this file to "pre-commit".
if git rev-parse --verify HEAD >/dev/null 2>&1
then
against=HEAD
else
# Initial commit: diff against an empty tree object
against=4b825dc642cb6eb9a060e54bf8d69288fbee4904
fi
#开始 代码检查拦截
TAG="pre-commit log----------------->"
#打印success日志函数
printS(){
echo -e "\033[0;32m $* \033[0m"
}
#打印fail日志函数
printE(){
echo -e "\033[0;31m $* \033[0m"
}
#获取脚本的根目录
SCRIPT_DIR=$(dirname "$0")
#echo "SCRIPT_DIR=$SCRIPT_DIR"
SCRIPT_ABS_PATH=`cd "$SCRIPT_DIR"; pwd`
#echo "SCRIPT_ABS_PATH=$SCRIPT_ABS_PATH"
#在app项目目录下执行 gradlew 命令执行 checkstyle task
$SCRIPT_ABS_PATH/../../gradlew -P checkCommit="true" checkstyle
if [ $? -eq 0 ]; then
# checkstyle检查成功
info=$TAG"checkstyle OK"
printS $info
else
#checkstyle检查失败
info=$TAG"checkstyle fail"
printE $info
exit 1
fi
#结束
# If you want to allow non-ASCII filenames set this variable to true.
allownonascii=$(git config --bool hooks.allownonascii)
# Redirect output to stderr.
exec 1>&2
# Cross platform projects tend to avoid non-ASCII filenames; prevent
# them from being added to the repository. We exploit the fact that the
# printable range starts at the space character and ends with tilde.
if [ "$allownonascii" != "true" ] &&
# Note that the use of brackets around a tr range is ok here, (it's
# even required, for portability to Solaris 10's /usr/bin/tr), since
# the square bracket bytes happen to fall in the designated range.
test $(git diff --cached --name-only --diff-filter=A -z $against |
LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0
then
cat <<\EOF
Error: Attempt to add a non-ASCII file name.
This can cause problems if you want to work with people on other platforms.
To be portable it is advisable to rename the file.
If you know what you are doing you can disable this check using:
git config hooks.allownonascii true
EOF
exit 1
fi
# If there are whitespace errors, print the offending file names and fail.
exec git diff-index --check --cached $against --
4. commit-msg脚本代码:
#!/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.
#开始 日志拦截
TAG="commit-msg log----------------->"
#打印success日志函数
printS(){
info=$TAG$*
echo -e "\033[0;32m $info \033[0m"
}
#打印fail日志函数
printE(){
info=$TAG$*
echo -e "\033[0;31m $info \033[0m"
}
fileName=$1
#message 获取提交日志
message=$(<$fileName)
if [ -z $message ]
#message 为空
then
printE "submit message info is null"
exit 1
fi
#例子,对下面的字符串模板解析
#[update|add|change|fix]xxx
printS $message
#日志最大长度
maxLen=30
#日志最小长度
minLen=4
sI=-1
eI=-1
#提交日志模板样式
logFormat="log format--->\"[update|add|change|fix]xxxxxxxx\""
#判断[]起始结束位置
sI=$(awk -v a="$message" -v b="[" 'BEGIN{print index(a,b)}')
eI=$(awk -v a="$message" -v b="]" 'BEGIN{print index(a,b)}')
#sI=`expr index "$message" [`
#eI=`expr index "$message" ]`
len=${#message}
eeI=`expr $eI-2`
#printS "sI=$sI,eI=$eI"
#判断[]开始
if [[ $sI -eq 1 && $eeI -ge $sI ]]
then
action=${message:$sI:$eeI}
log=${message:$eI}
logLen=${#log}
#printS "action=$action,log=$log,logLen=$logLen"
#提交action判断
if [[ $action = "update" || $action = "add" || $action = "change" || $action = "fix" ]]
then
subLog=${log:0:1}
#printS "log=$log,subLog=$subLog"
#提交的本次记录内容
if [ -z $log ]
then
printE "log is empty"
exit 1
elif [ $logLen -lt $minLen ]
#提交的本次记录内容太少
then
printE "log is min $minLen"
exit 1
elif [ -z $subLog ]
#提交的本次记录内容与】之间有空格,[update] XXXX
then
printE $logFormat
exit 1
elif [ $logLen -gt $maxLen ]
#提交的本次记录内容太多
then
printE "log is max $maxLen"
exit 1
else
printS "check log success"
fi
else
printE $logFormat
exit 1
fi
else
printE $logFormat
exit 1
fi
#结束
5. hooks脚本代码:
#!/bin/bash
#开始
# 复制 pre-commit commit-msg脚本到/.git/hooks/目录下
# r d
action='r'
#获取输入命令的第一个参数
cmd=$1
if [ $cmd ]
then
action=$1
fi
echo "action=$action"
# 获取hook下的commit-msg和pre-commit
precommitName="pre-commit"
commitName="commit-msg"
#获取当前目录
sourcePath=$(pwd)
sourcePrecommit=$sourcePath"/"$precommitName
sourceCommitmsg=$sourcePath"/"$commitName
echo "sourcePrecommit=$sourcePrecommit"
echo "sourceCommitmsg=$sourceCommitmsg"
cd ..
rootPath=$(pwd)
echo "rootPath=$rootPath"
gitDir=$rootPath"/.git"
echo "gitDir=$gitDir"
hooksDir=$gitDir"/hooks"
echo "hooksDir=$hooksDir"
pre_hooks=$hooksDir"/"$precommitName
commit_hooks=$hooksDir"/"$commitName
# 创建一个.git目录
if [ -d $gitDir ]
then
echo "$gitDir exsit"
if [ -d $hooksDir ]
then
echo "$hooksDir exsit"
else
echo "$hooksDir not exsit"
# mkdir 创建一个新目录
mkdir $hooksDir
echo "$hooksDir mkdir ok"
fi
else
echo "$gitDir not exsit"
git init
echo "git init ok"
fi
#复制 pre_commit 文件
if [ -e $pre_hooks ]
then
echo "$pre_hooks exsit"
if [ $action = "d" ]
then
rm -r $pre_hooks
echo "$pre_hooks remove ok"
else
cp $sourcePrecommit $hooksDir
chmod 777 $pre_hooks
echo "$pre_hooks replace ok"
fi
else
echo "$pre_hooks not exsit"
cp $sourcePrecommit $hooksDir
chmod 777 $pre_hooks
echo "$pre_hooks copy ok"
fi
#复制 commit-msg 文件 -e 判断文件是否存在
if [ -e $commit_hooks ]
then
echo "$commit_hooks exsit"
if [ $action = "d" ]
then
# rm -r 递归删除
rm -r $commit_hooks
echo "$commit_hooks remove ok"
else
# cp复制
cp $sourceCommitmsg $hooksDir
chmod 777 $commit_hooks
echo "$commit_hooks replace ok"
fi
else
echo "$commit_hooks not exsit"
cp $sourceCommitmsg $hooksDir
chmod 777 $commit_hooks
echo "$commit_hooks copy ok"
fi
#结束
三.如何集成到项目以及演示(以下演示都是基于Android studio以及Mac系统)
1.Hook下5个配置文件在Project下的位置如图:
2.运行hooks.sh,把pre-commit & commit-msg配置到 .git下。
第一步.找到Project下 .git中的hooks查看里面内容(如果没有初始化git,可以在命令行输入git init, 也可以不初始化,hooks.sh脚本中配置了初始化git)。
第二步.在Android studio中的Terminal运行命令cd hook,进入hook文件。
第三步.输入chmod 777 hooks.sh,给hooks.sh脚本增加可执行权限。
第四步.输入./hooks.sh。这时候我们再打开 .git下的hooks会发现pre-commit & commit-msg已经copy到该目录下
第五步.输入cd ..,退出hook文件夹
3.编写一份Test代码,然后输入 git add. + git commit -m test提交(提交失败)
package hulk.com;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.text.TextUtils;
import android.widget.Toast;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
public void test(String s1, String s2, String s3, String s4, String s5, String s6) {
}
}
4.修改error级别错误代码后,然后输入 git add. + git commit -m test提交(提交失败)
5.输入 git add. + git commit -m [update]test提交
四.除了用shell脚本语言配置Hooks外,是否还能用其他脚本语言进行配置?
内置的脚本大多都是shell和PERL语言,但是我们可以使用任何脚本语言包括当前市场上最火热的Python语言,只要它们最后能编译到可执行文件当中即可。每次脚本中的#!/bin/sh定义了你的文件将被如何解释。比如,使用其他语言时你只需要将path改为你的解释器的路径。
五.Hooks检查的优势之处与不足之处在哪
相对与代码的自测,Hooks检查可谓是进了一大步,它是强制,一次配置终身检查,它是自动的,即我们每次提交代码时它都会自动拦截,它不但可以对你提交的内容进行检查,还可以对你提交的日志进行规范,使我们的每一次提交变得规范与严谨。但是它还是有不足之处,第一,它只能对当前提交的代码进行检查,我们无法追溯历史代码;第二,如果开发人员不对Hooks进行配置或者对配置好的Hooks进行修改,那么可以达到逃避检查的目的。针对以上两点,我们就需要引入Jenkins打包时对代码进行进一步扫描。