git 服务端钩子做代码检查

需求分析

在代码修改后可以对代码进行检查,比如代码规范检查、代码构建、单元测试等。我们需要禁止成员推送不符合规范的代码到服务端。

Git 钩子能在特定的重要动作发生时触发自定义脚本。钩子分为客户端和服务器端两类。使用客服端钩子可以在commit时,对本地代码进行检查,可以参考:使用git钩子对提交代码进行检查。考虑到客服端钩子需要每个成员单独配置,或者说不是一种强制手段,无法避免某成员跳过钩子,强制push代码到远程的行为,我们将主要研究如何使用服务端钩子来拦截非法push的问题。

服务端钩子介绍

钩子都被存储在 Git 目录下的 hooks 子目录中。 也即绝大部分项目中的 .git/hooks 。

下图是使用gitolite搭建的git服务端,里面有一些示例钩子。

如需使钩子生效,需把文件.sample后缀去掉,如update.sample改成update,并确保文件有可执行权限。钩子脚本可以使用Shell、Python、Perl等脚本实现。这些脚本会在特定的动作发生时被调用,还会传递一些重要的参数,方便后续使用。

各个钩子的介绍

pre-receive

处理来自客户端的推送操作时,最先被调用的脚本是 pre-receive。 它从标准输入获取一系列被推送的引用。如果它以非零值退出,所有的推送内容都不会被接受。 你可以用这个钩子阻止对引用进行非快进(non-fast-forward)的更新,或者对该推送所修改的所有引用和文件进行访问控制。

update

update 脚本和 pre-receive 脚本十分类似,不同之处在于它会为每一个准备更新的分支各运行一次。 假如推送者同时向多个分支推送内容,pre-receive 只运行一次,相比之下 update 则会为每一个被推送的分支各运行一次。 它不会从标准输入读取内容,而是接受三个参数:引用的名字(分支),推送前的引用指向的内容的 SHA-1 值,以及用户准备推送的内容的 SHA-1 值。 如果 update 脚本以非零值退出,只有相应的那一个引用会被拒绝;其余的依然会被更新。

post-receive

post-receive 挂钩在整个过程完结以后运行,可以用来更新其他系统服务或者通知用户。 它接受与 pre-receive 相同的标准输入数据。 它的用途包括给某个邮件列表发信,通知持续集成(continous integration)的服务器, 或者更新问题追踪系统(ticket-tracking system) —— 甚至可以通过分析提交信息来决定某个问题(ticket)是否应该被开启,修改或者关闭。 该脚本无法终止推送进程,不过客户端在它结束运行之前将保持连接状态, 所以如果你想做其他操作需谨慎使用它,因为它将耗费你很长的一段时间。

实现需求

从钩子的介绍来看,我们需要拦截推送,可以在pre-receive和update中通过返回值来标示是否需要拦截。

服务端钩子会接受到三个参数:

  • 被推送的引用的名字

  • 推送前分支的修订版本(revision)

  • 用户准备推送的修订版本(revision)

我们使用pre-receive来实现,可以使用shell脚本获取到传递的进来的参数,可以参考gitlab pre-receive钩子

# pre-receive

#!/bin/bash

while read oldVersion newVersion branch; do
    echo ${oldVersion}
    echo ${newVersion}
    echo ${branch}
done

此时我们虽然可以拿到最新的commit id,也就是最新提交的ref,但是如何把代码取出来呢?

在服务端仓库中,不能直接拿到提交文件,而是以git object存储的。

可以使用git diff 拿到修改内容的差异文件。但是当我们需要对整个代码做检查时,需要的不仅是差异文件,可以在服务器上再clone出一套代码存放在build_code文件夹中,再把本次更新的差异应用到clone出来的代码上。

git@xxx:~/build_code$ git clone ~/repositories/testing.git
Cloning into 'testing'...
done.
git@xxx:~/build_code$ cd testing/
git@xxx:~/build_code/testing$ git branch 
* master
git@xxx:~/build_code/testing$ 

注意:clone代码后,需确认当前代码的分支,这里默认使用master,如需改成其它分支,需checkout到对应的分支。

由于git diff 生成的文件不能记录新增和删除的文件,这里我们可以用尝试使用git patch打包,然后在clone的代码端apply patch。

# pre-receive

#!/bin/bash

while read oldVersion newVersion branch; do
    # 只对master分支做检查
    result=$(echo ${branch}| grep "master")
    if [ "$result" != "" ];then
        echo 开始检查代码
        # 生成patch
        git format-patch $oldVersion..$newVersion
        
        cd ~/build_code/testing/
        unset GIT_DIR
        unset GIT_QUARANTINE_PATH
        git reset HEAD --hard
        # 应用patch
        git apply  ~/repositories/testing.git/*.patch
        # 清空patch
        rm -rf ~/repositories/testing.git/*.patch
        #开始验证代码
        bash check_code.sh # 自定义的检查脚本
        if [ $? -ne 0 ]; then
            echo 检查代码失败
            exit 1
        fi
        echo 检查代码成功
    fi
done

到这里,我们就做到了对push到远程的代码进行检查检查的功能,若检查失败会exit 1,使成员在客服端上push时,操作失败。

后续问题

简单测试使用,并未发现异常,但是当客户端push的代码和当前ref差异较大时(如有分支分叉情况),apply patch会失败,导致检查的代码并不是最新代码。所以我们必须改变思路,放弃使用patch的方式。

修改思路

通过git diff 获取修改、删除、增加、重命名文件的名字,然后在clone到build_code中的代码中,重现对应的操作。

查看修改文件的记录git diff --name-status $oldVersion $newVersion,可参考Git pre-commit hook : changed/added files

修改后的代码:

# pre-receive

#!/bin/bash

mkdir_and_cp_file(){
    result=$(echo $1 | grep "/")
    if [ "$result" != "" ];then
        mkdir -p $2/${1%/*}
        echo mkdir_and_cp_file:$1
    fi
    
    # tempDir=temp
    # mkdir -p $tempDir
    # git archive --format tar.gz --output "$tempDir/output.tar.gz" $newVersion $1
    # tar zxf $tempDir/output.tar.gz -C $tempDir
    # cp  $tempDir/$1 $2/$1
    # rm -rf $tempDir

    git show $newVersion:$1 > $2/$1

}

while read oldVersion newVersion branch; do
    # 只对master分支做检查
    result=$(echo ${branch}| grep "master")
    if [ "$result" != "" ];then
        echo 开始检查代码
        desPath=~/build_code/testing/
        # echo -e "\ncp file"
        gitDiff=`git diff --name-status $oldVersion $newVersion | awk '$1 == "M" { print $2 }'`
        
        for var in ${gitDiff}; do
            mkdir_and_cp_file ${var} $desPath
        done
        
        # echo -e "\nmkdir and add cp file"
        gitDiff=`git diff --name-status $oldVersion $newVersion | awk '$1 == "A" { print $2 }'`
        for var in ${gitDiff}; do
            mkdir_and_cp_file ${var} $desPath
        done
        
        gitDiff=`git diff --name-status $oldVersion $newVersion | awk '$1 ~ "R" { print $3}'`
        for var in ${gitDiff}; do
            mkdir_and_cp_file ${var} $desPath
        done
        
        # echo -e "\ndelete file"
        gitDiff=`git diff --name-status $oldVersion $newVersion | awk '$1 == "D" { print $2 }'`
        for var in ${gitDiff}; do
            rm $desPath/${var}
        done
        
        gitDiff=`git diff --name-status $oldVersion $newVersion | awk '$1 ~ "R" { print $2}'`
        for var in ${gitDiff}; do
            rm $desPath/${var}
        done
        
        cd $desPath
        unset GIT_DIR
        unset GIT_QUARANTINE_PATH
        #开始验证代码
        bash check_code.sh # 自定义的检查脚本
        if [ $? -ne 0 ]; then
            echo 检查代码失败
            exit 1
        fi
        echo 检查代码成功
    fi
done

另外需要注意,build_code中clone的代码,在pre-receive中检查后,需要在代码确认和入后(也就是post-receive后)将此份代码同步到最新状态,以便在下次使用git diff重现对应操作时,代码是干净且最新状态。

以上是使用服务端钩子检查代码的一种方式,有更好的方法,欢迎提出。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值