一、前言
注意:这篇文章仅针对IOS开发
这段时间一直在研究如何利用GitHook来实现代码质量检查 ,由于对shell脚本和python的不熟悉,也踩了不少坑。直到今天,总算有了些初步成果。就在这分享下~ 哈哈 ~
先说说实现的效果吧,主要有两个方面:
1.Git 提交信息(git commit message)的检查
这个主要是用正则表达式,对提交信息做一个匹配~匹配不通过就不让提交
这篇文章实现的效果
【机能追加】【xxxx】xxxx
【BUG修正】【xxxx】xxxx
【样式变更】 【xxxx】xxxx
【重构】【xxxx】xxxx
只有上面4种格式的提交信息才可以匹配通过,允许提交
正则表达式如下:
['^\\[机能追加\\] *\\[.+\\] *.+', '^【机能追加】 *【.+】 *.+',
'^\\[BUG修正\\] *\\[.+\\] *.+', '^【BUG修正】 *【.+】 *.+',
'^\\[样式变更\\] *\\[.+\\] *.+', '^【样式追加】 *【.+】 *.+',
'^\\[重构\\] *\\[.+\\] *.+', '^【重构】 *【.+】 *.+']
2. OC的代码检查
主要是检查代码是否有警告和报错,和最近一次编译时间
XCode编译时,会生成编译的log,我们会把最近一次的编译log拿来分析。如果最近一次编译时间是在你提交代码前2个小时,那当然不会让你提交的。如果最近一次编译的log里有大量警告或者错误,也不会让你提交。主要是为了避免粗心,把一些不规范的代码提交到Git上
二、实现
1、打开项目中Git的隐藏文件夹
在项目的目录下,有一个.git的隐藏文件夹,我们需要在这个文件夹下的Hook文件夹里添加我们自己的脚本,这样,才会触发GitHook,使我们的脚本生效
怎么打开?
方法一:打开Finder,转到Macintosh HD文件夹(从左栏中的设备访问),按住Cmd + Shift +.,所有隐藏文件都将变为可见,按住Cmd + Shift +.,第二次再次隐藏文件。
方法二: 在终端键入命令:chflags nohidden projectpath 。projectpath 是你的项目路径
打开隐藏文件后,我们在项目的隐藏文件夹git下会发现一个hook文件夹,如果没有cd到项目,git init下,就会有hook文件夹~打开hook文件夹后,里面都是后缀为.sample的文件,这些都是GitHook脚本~我们可以尝试下把.sample后缀去掉,这样的话,它们在Git提交时会被执行
2、脚本实现
有4个脚本,commit-msg.sh , reg_push_msg.py , pre-commit.sh , analyse_oc_buildlog.py,脚本的命名最好别改,特别是两个sh脚本,两个sh脚本的名字改成其他的,Git提交时就不会触发~
其中commit-msg.sh和pre-commit.sh这两个shell脚本,我们把它放到hook文件下,并且修改它们的权限,使得Git提交时,可以自动触发~
怎么修改权限?
在终端执行以下命令:
mv commit-msg.sh commit-msg
chmod 777 commit-msg
mv pre-commit.sh pre-commit
chmod 777 pre-commit
再然后,在hook目录下,新建一个文件夹Script~
把两个python脚本放到新建文件夹Script里~
这两个脚本你也可以放到别处,对应修改下sh脚本中的python路径~
最后将analyse_oc_buildlog.py脚本中的DerivedData路径和project_name换成自己的~
至此,大功告成~
代码如下:
1、提交信息脚本实现
sh脚本
commit-msg.sh
Git提交时,会触发commit-msg.sh脚本~
commit-msg.sh脚本会去执行reg_push_msg.py脚本,并会接收python脚本执行完后传过来的参数,并根据参数来判断提交信息是否符合规范~若符合规范,exit(0),退出脚本,Git提交流程会继续走下去,若不符合规范,exit(1),Git提交会被中断。
至于为什么会执行commit-msg.sh脚本,可以参考GItHook~
https://www.git-scm.com/book/zh/v2/自定义-Git-Git-钩子
#!/bin/sh
# 接收git-commit传过来的参数 message
fileName=$1
message=$(<$fileName)
# 获取当前路径
project_path=$(cd `dirname $0`; pwd)
# 拼接python路径
python_dir_path="/Script/reg_push_msg.py"
python_path=$project_path$python_dir_path
# 执行python
python $python_path $message
# shell 接收python脚本参数
if [ $? == "0" ];then
exit 0
else
exit 1
fi
python脚本
reg_push_msg.py
reg_push_msg.py脚本主要是利用正则表达式校验提交信息是否符合规范~
它会接受commit-msg.sh脚本传过来的提交信息,并对提交信息用正则表达式判断是否符合规范,若符合规范,exit(0),不符合规范,exit(1),并将0或者1传给commit-msg.sh脚本~
# coding = utf-8
# encoding:utf-8
import sys
import re
# 设置编码格式
reload(sys)
sys.setdefaultencoding('utf8')
# 正则表达式
regular_list = ['^\\[机能追加\\] *\\[.+\\] *.+', '^【机能追加】 *【.+】 *.+',
'^\\[BUG修正\\] *\\[.+\\] *.+', '^【BUG修正】 *【.+】 *.+',
'^\\[样式变更\\] *\\[.+\\] *.+', '^【样式追加】 *【.+】 *.+',
'^\\[重构\\] *\\[.+\\] *.+', '^【重构】 *【.+】 *.+']
# 解析shell脚本传过来的参数
def parseArgument():
if len(sys.argv) < 2:
print('shell脚本没有传参数\n')
raise Exception("参数是必须的!")
# init
argus = {}
argus["message"] = u""
# set
argv_msg = u''
for tempstr in sys.argv[1:]:
argv_msg += tempstr
argus["message"] = argv_msg
return argus
def reg_message(message, restr):
temp = message.decode('utf8')
pat = restr.decode('utf8')
pattern = re.compile(pat, flags=re.IGNORECASE)
results = pattern.findall(temp)
return len(results)
if __name__ == "__main__":
argus = parseArgument()
push_message = argus['message']
print('\n\n执行提交信息校验 \n')
print('\n提交信息:{0}\n'.format(push_message))
reg_result = False
for reg in regular_list:
if reg_message(push_message, reg) > 0:
reg_result = True
if reg_result:
exit(0)
else:
print('*' * 50)
print(' ❌❌❌ 提交信息格式不正确 禁止提交代码 ❌❌❌ ')
print('*' * 50)
print('\n')
print('------------------------------------')
print(' 请参考以下格式 重新提交: \n')
print('[功能追加][通用]加入英文语言支持 \n')
print('[BUG修正][XF01]解决车机定制需求引起的XX bug \n')
print('[样式变更][XF03]新增定制需求,加入广播 \n')
print('[重构] [通用]增加xx类代码注释 \n')
print('------------------------------------')
exit(1)
2、检查OC代码脚本
sh脚本
pre-commit.sh
Git提交时,会触发pre-commit.sh脚本~
pre-commit.sh脚本会去执行analyse_oc_buildlog.py脚本,并会接收python脚本执行完后传过来的参数,并根据参数来判断代码是否符合规范~若符合规范,exit(0),退出脚本,Git提交流程会继续走下去,若不符合规范,exit(1),Git提交会被中断
#!/bin/bash
# 获取当前路径
project_path=$(cd `dirname $0`; pwd)
# 拼接python路径
python_dir_path="/Script/analyse_oc_buildlog.py"
python_path=$project_path$python_dir_path
# 执行python
python $python_path
# shell 接收python脚本参数
if [ $? == "0" ];then
exit 0
else
exit 1
fi
python脚本
analyse_oc_buildlog.py
analyse_oc_buildlog.py脚本需要将其中的DerivedData路径换成自己的
analyse_oc_buildlog.py脚本,先会在Xcode的DerivedData中,获取所有以.xcactivitylog为后缀的log文件,并将它们按时间排序,拿到最新的编译log文件。如果当前时间比最新log文件的创建时间大2个小时以上,即判断为提交不规范,exit (1),退出python脚本,反之,则脚本会继续执行下去。再然后,会在终端中,执行相关的命令,将.xcactivitylog文件转成.log文件~
最后,逐行读取log文件,利用正则表达式匹配所有的‘xx errors generated’ 和‘ yy warnings generated'’,将所有xx加起来,即为编译的错误数,将所有的yy加起来,即为编译的警告数~若错误数和警告数都等于0,则exit(0),反之exit(1);并将0或者1传给pre-commit.sh脚本
# coding = utf-8
# encoding:utf-8
import os
import re
import string
import time
project_name = 'GitHookTest' #eDriveGWM #eDrive40
derived_data_path = '/Users/shizhongqiu/Library/Developer/Xcode/DerivedData'
file_type = '.xcactivitylog'
# 对文件夹里的文件按创建时间排序
def sort_deriveddir_list(deriveddir_path):
# 获取dervieddata文件夹下的所有文件
derived_filepath_list = os.listdir(deriveddir_path)
loglist = []
for derivedfile_name in derived_filepath_list:
# 获取dervied文件夹下 与项目的相关的文件夹
if string.rfind(derivedfile_name, project_name) != -1:
# 拼接log的地址
alogdir_path = os.path.join(deriveddir_path, '{dfilename_key}/Logs/Build'.format(dfilename_key=derivedfile_name))
# 获取这个log地址下 所有文件
if os.path.isdir(alogdir_path) > 0:
path_list = os.listdir(alogdir_path)
# 遍历所有文件 获取.xcactivitylog文件
for afilename in path_list:
if os.path.splitext(afilename)[1] == file_type:
afilepath = os.path.join(alogdir_path, afilename)
loglist.append(afilepath)
if not loglist:
return
else:
# 注意,这里使用lambda表达式,将文件按照最后修改时间顺序升序排列
# os.path.getmtime() 函数是获取文件最后修改时间
# os.path.getctime() 函数是获取文件最后创建时间
dir_list = sorted(loglist, key=lambda x: os.path.getctime(x), reverse=True)
# print(dir_list)
return dir_list
# # 对文件夹里的文件按创建时间排序
# def sort_file_list(file_path):
# path_list = os.listdir(file_path)
#
# loglist = []
# for filename in path_list:
# if os.path.splitext(filename)[1] == file_type:
# loglist.append(filename)
#
# dir_list = list(loglist)
#
# if not dir_list:
# return
# else:
# # 注意,这里使用lambda表达式,将文件按照最后修改时间顺序升序排列
# # os.path.getmtime() 函数是获取文件最后修改时间
# # os.path.getctime() 函数是获取文件最后创建时间
# dir_list = sorted(dir_list, key=lambda x: os.path.getctime(os.path.join(file_path, x)), reverse=True)
# # print(dir_list)
# return dir_list
def find_error_inline(aline):
temp = aline.decode('utf8')
findword = r'[1-9]+ *errors *generated'
pattern = re.compile(findword)
results = pattern.findall(temp)
if len(results) > 0:
num = 0
for tempresult in results:
print(tempresult)
num = num + int(tempresult.split()[0])
print('编译错误:{num_key}\n'.format(num_key=num))
return num
else:
return 0
def find_warn_inline(aline):
findword = r'[1-9]+ *warnings *generated'
pattern = re.compile(findword)
results = pattern.findall(aline)
if len(results) > 0:
num = 0
for tempresult in results:
print(tempresult)
num = num + int(tempresult.split()[0])
print('编译错误:{num_key}\n'.format(num_key=num))
return num
else:
return 0
print('\n\n')
print('执行OC编译日志检查\n\n')
sort_log_list = sort_deriveddir_list(derived_data_path)
print('所有的日志文件,按时间倒序排列\n')
for filename in sort_log_list:
print('文件名:{file_name_key} 创建时间:{file_ctime_key}\n'.format(file_name_key=filename, file_ctime_key=os.path.getctime(filename)))
#获取文件夹中 最新的文件
newfilepath = sort_log_list[0]
print('最新的日志文件Path:{file_name_key}\n'.format(file_name_key=newfilepath))
# 判断最新编译时间
nowtimesp = int(time.time())
newfile_ctimesp = os.path.getctime(newfilepath)
print('当前时间:{nowtime_key} 最新编译时间:{newfile_time_key} \n'.format(nowtime_key=nowtimesp, newfile_time_key=newfile_ctimesp))
if nowtimesp > newfile_ctimesp + 2*3600:
print('*'* 50)
print('️️️ 项目长时间未编译 禁止提交 ️️️ ️')
print('*'* 50)
print('\n\n')
exit(1)
print('\n执行命令行命令 将.xcactivitylog文件转成.log \n')
#执行命令行命令
#将.xcactivitylog 转成 .log 文件
cmd = 'gunzip -c {newfilename_key} > {spath_key}/{project_key}build.log'.format(newfilename_key=newfilepath, spath_key=derived_data_path, project_key=project_name)
print('打印命令行命令:{cmd_key}\n'.format(cmd_key=cmd))
cmd_result = os.system(cmd)
print('命令行执行结果:{cmdresult_key}\n'.format(cmdresult_key=cmd_result))
#读取build.log 文件 查找警告及错误个数
warnnum = 0
errornum = 0
buildlogpath = '{tpath}/{project_key}build.log'.format(tpath=derived_data_path, project_key=project_name)
with open(buildlogpath) as f:
for line in f.readlines():
a = find_warn_inline(line)
b = find_error_inline(line)
warnnum = warnnum + a
errornum = errornum + b
pass
if errornum > 0:
print('*'* 50)
print('❌❌❌ 代码存在报错 禁止提交代码 {num_key} ❌❌❌ '.format(num_key=errornum))
print('*'* 50)
print('\n\n')
exit(1)
else:
if warnnum > 0:
print('*'* 50)
print('️️️ 代码存在大量警告 禁止提交代码 {num_key} ️️️'.format(num_key=warnnum))
print('*'* 50)
print('\n\n')
exit(1)
else:
print('*'* 50)
print(' 优秀 代码质量杠杠的 ')
print('*'* 50)
print('\n\n')
exit(0)
后期可以考虑将OCLint结合起来,利用OCLint来进行代码分析,那就完美了~