美文网首页
GitHook 实现代码质量检查和提交规范 初探

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

作者: maskerII | 来源:发表于2019-05-10 20:22 被阅读0次

    一、前言

    注意:这篇文章仅针对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来进行代码分析,那就完美了~

    相关文章

      网友评论

          本文标题:GitHook 实现代码质量检查和提交规范 初探

          本文链接:https://www.haomeiwen.com/subject/eyjzoqtx.html