GitHook 实现代码质量检查和提交规范 初探

一、前言

注意:这篇文章仅针对IOS开发

这段时间一直在研究如何利用GitHook来实现代码质量检查 ,由于对shell脚本和python的不熟悉,也踩了不少坑。直到今天,总算有了些初步成果。就在这分享下~ 哈哈 ~

先说说实现的效果吧,主要有两个方面:

1.Git 提交信息(git commit message)的检查

这个主要是用正则表达式,对提交信息做一个匹配~匹配不通过就不让提交
这篇文章实现的效果
【机能追加】【xxxx】xxxx
【BUG修正】【xxxx】xxxx
【样式变更】 【xxxx】xxxx
【重构】【xxxx】xxxx
只有上面4种格式的提交信息才可以匹配通过,允许提交
正则表达式如下:

        ['^\\[机能追加\\] *\\[.+\\] *.+', '^【机能追加】 *【.+】 *.+',
               '^\\[BUG修正\\] *\\[.+\\] *.+', '^【BUG修正】 *【.+】 *.+',
               '^\\[样式变更\\] *\\[.+\\] *.+', '^【样式追加】 *【.+】 *.+',
               '^\\[重构\\] *\\[.+\\] *.+', '^【重构】 *【.+】 *.+']


屏幕快照 2019-05-13 下午4.17.43.png

2. OC的代码检查

主要是检查代码是否有警告和报错,和最近一次编译时间
XCode编译时,会生成编译的log,我们会把最近一次的编译log拿来分析。如果最近一次编译时间是在你提交代码前2个小时,那当然不会让你提交的。如果最近一次编译的log里有大量警告或者错误,也不会让你提交。主要是为了避免粗心,把一些不规范的代码提交到Git上

屏幕快照 2019-05-13 下午4.19.31.png

二、实现

屏幕快照 2019-05-13 下午4.14.21.png
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来进行代码分析,那就完美了~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,881评论 4 368
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,052评论 1 301
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 110,598评论 0 250
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,407评论 0 217
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,823评论 3 294
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,872评论 1 224
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,037评论 2 317
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,778评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,505评论 1 247
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,745评论 2 253
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,233评论 1 264
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,568评论 3 260
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,231评论 3 241
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,141评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,939评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,954评论 2 283
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,784评论 2 275

推荐阅读更多精彩内容