环境说明
版本使用说明
- 
Gitlab version:  13.10.2
gitlab 服务器安装教程请参考 文档,本文档将示例在 gitlab 中如何添加和配置 pre-receive 钩子,并演示针对 git push 操作做相关的限制操作。
 
文档参考
Pre-receive 钩子概述
当我们需要对 git 提交的相关内容做校验时,可以使用服务端的 pre-receive 钩子。相对于客户端的 pre-commit 钩子来说,它便于统一管理维护。pre-commit 需要将处理脚本添加到客户端代码仓库的 .git/hooks 目录下,当 clone 后 需要重新 进行添加,所以通常采用服务端的 pre-receive 来替代。
pre-receive 可以对 push 中的内容做校验,如果校验脚本正常退出(exit 0)即允许push,反之会拒绝客户端进行push操作。
在 gitlab  服务端中除了 pre-receive 钩子外还可以添加其他两种钩子即 pre-receive 和 update 钩子,详细文档说明请 参考
Pre-receive 钩子添加
pre-receive  脚本的使用可以是 shell、python、ruby 等任何可执行程序,需要注意它的权限。pre-receive 钩子会从标准输入获取三个参数:旧版本号,新版本号,分支。同时还提供了内置的许多环境变量可以使用,详情可以参考官方文档。
Gitlab 服务端开启 自定义全局钩子选项
下面示例中的 gitlab 使用 docker 进行部署,数据通过映射持久卷的形式进行保存。
gitlab 中有 全局 的 webhook 和 针对 单个项目生效的 webhook,针对单个项目生效的 hook 我后面将会有说明。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
  | 
$ pwd
/application/gitlab
$ ll
total 4
drwxrwxr-x  3 root root  239 Apr  9 16:38 config
drwxr-xr-x 20 root root 4096 Apr 13 10:14 data
drwxr-xr-x 20 root root  326 Apr  9 16:41 logs
vi config/gitlab.rb 
gitlab_shell['custom_hooks_dir'] = "/etc/gitlab/hooks"  # 设置全局钩子文件夹,默认这行在配置文件中是注释的
mkdir -p config/hooks/pre-receive.d/  # 创建文件夹
docker exec -it gitlab-ce bash -c 'ls /etc/gitlab/hooks'  # 查看容器中此文件夹是否存在,并验证映射关系是否正确
pre-receive.d 
docker exec -it gitlab-ce bash -c 'gitlab-ctl reconfigure' # 配置重载生效,不执行此项配置将不会生效,重启容器也没有用。
docker restart gitlab-ce   # 重启一下相关容器
  | 
 

添加全局 commit 字节长度验证
此脚本为 shell 脚本,脚本示例如下所示
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
  | 
cat config/hooks/pre-receive.d/commit-length 
#!/bin/bash
#pre-receive script
#set -x #for debugging
LG=18  # 字节长度
validate_ref()
{
    # --- Arguments
    oldrev=$(git rev-parse $1)
    newrev=$(git rev-parse $2)
    refname="$3"
    commitList=`git rev-list $oldrev..$newrev`
        #echo $commitList
    split=($commitList)
    for s in ${split[@]}
    do
        echo "@@@@@@@"
        echo "$s"
        msg=`git cat-file commit $s | sed '1,/^$/d'`
        echo $msg
        if [ ${#msg} -lt "$LG" ];then
            echo "!!! Commit message length less than 18"
            exit 1
        else
            echo "bigger than $LG by commit-length check"
        fi
    done
}
fail=""
# Allow dual mode: run from the command line just like the update hook, or
# if no arguments are given then run as a hook script
if [ -n "$1" -a -n "$2" -a -n "$3" ]; then
    # Output to the terminal in command line mode - if someone wanted to
    # resend an email; they could redirect the output to sendmail
    # themselves
    PAGER= validate_ref $2 $3 $1
else
    while read oldrev newrev refname
    do
        validate_ref $oldrev $newrev $refname
    done
fi
if [ -n "$fail" ]; then
    exit $fail
fi
docker exec -it gitlab-ce bash -c 'chmod 777 -R /etc/gitlab/hooks/'
  | 
 
这里需要注意一下 1 个 中文字符 等于 3个 字节,也就是说最少需要 8 中文字符,当然 空格 也会被计入,如想排除空格,可修改一下脚本对应逻辑即可,这里不多赘述实现方法,熟悉 shell 脚本的人应该不难。
这里的 权限 一定要,进行更改一下,否则将导致 hook 脚本不生效。
验证脚本是否可行
gitlab dashboard 验证

可以看到,已经提交不上去了,只是 dashboard 中无 message 错误输出。
命令行验证
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
  | 
cd /tmp
git clone http://gitlab.treesir.pub/gitlab-instance-7c228ebb/Monitoring.git
touch test.md
git add ./*
git commit -m "aaa"
git push origin master  # 可以看到 命令行是有正常输出的。
Enumerating objects: 4, done.
Counting objects: 100% (4/4), done.
Delta compression using up to 4 threads
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 266 bytes | 266.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0)
remote: @@@@@@@
remote: d1d9d8d5cad9ece2e82e4a9252efacb271c4fd0f
remote: aaa
remote: !!! Commit message length less than 18
To http://gitlab.treesir.pub/gitlab-instance-7c228ebb/Monitoring.git
 ! [remote rejected] master -> master (pre-receive hook declined)
error: failed to push some refs to 'http://gitlab.treesir.pub/gitlab-instance-7c228ebb/Monitoring.git'
  | 
 
基于针对单个项目的 hook
有些时候我们的限制不想针对于全局,只想针对于某个项目组,或者组中的某些项目。下面示例,创建一个新的仓库,并对这个仓库添加独立的 hook 限制。全局钩子 和 局部钩子 的脚本方式是一样的,只是放置的路径不同,全局即我们上面配置的容器中 /etc/gitlab/hooks 而 局部的 是在 项目绝对路径/custom_hooks/ 下,可能局部的钩子,这样说有点抽象,下面我演示一下就清楚了。
添加局部 hook,强制 git commit 输入 jira issue id
此 hook 常见与 gitlab 与 jira 进行集成后,并且 jira issue id 是有规律的,我们可以通过正则进行过滤匹配即可。
shell 脚本如下所示
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
  | 
cat custom_hooks/pre-receive.d/require-jira-issue 
#!/bin/bash
#
# Reject pushes that contain commits with messages that do not adhere
# to the defined regex.
# This can be a useful pre-receive hook [1] if you want to ensure every
# commit is associated with a ticket ID.
#
# As an example this hook ensures that the commit message contains a
# JIRA issue formatted as [JIRA-<issue number>].
#
# [1] https://help.github.com/en/enterprise/user/articles/working-with-pre-receive-hooks
#
set -e
zero_commit='0000000000000000000000000000000000000000'
msg0_regex='^DT-[0-9]+ '
msg1_regex='^Merge'
while read -r oldrev newrev refname; do
        # Branch or tag got deleted, ignore the push
    [ "$newrev" = "$zero_commit" ] && continue
    # Calculate range for new branch/updated branch
    [ "$oldrev" = "$zero_commit" ] && range="$newrev" || range="$oldrev..$newrev"
        for commit in $(git rev-list "$range" --not --all); do
                if ! git log --max-count=1 --format=%B $commit | egrep -iq "$msg0_regex|$msg1_regex"; then
                        echo "ERROR:"
                        echo "ERROR: Your push was rejected because the commit"
                        echo "ERROR: $commit in ${refname#refs/heads/}"
                        echo "ERROR: is missing the JIRA Issue. Example: 'DT-123 This is a commit example.'"
                        echo "ERROR:"
                        echo "ERROR: Please fix the commit message and push again."
                        echo "ERROR: https://help.github.com/en/articles/changing-a-commit-message"
                        echo "ERROR"
                        exit 1
                fi
        done
done
  | 
 
找到对应项目在磁盘中的路径
从 gitlab 多少版本开始,项目的路径就是已 hash 形式存在磁盘中,目的好像是为了提高磁盘io性能,我看官方文档好像是这样说的。
创建一个新的项目

找到刚才这个项目的 对应 hash 路径



获得 hash 路径即 @hashed/d4/73/d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35.git
添加钩子
1
2
  | 
 cd /application/gitlab/data/git-data/repositories/ \
 && cd @hashed/d4/73/d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35.git  # 刚才找到的路径
  | 
 

创建 hook 文件夹
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
  | 
mkdir -p custom_hooks/pre-receive.d/ 
vim custom_hooks/pre-receive.d/require-jira-issue # 将上面的脚本内容 添加至文件夹中
tree custom_hooks
custom_hooks/
└── pre-receive.d
    └── require-jira-issue
1 directory, 1 file
chmod 777 -R ./custom_hooks  # 授予权限,同理权限未设置将导致,hook无效。
  | 
 
测试效果
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
  | 
git clone http://gitlab.treesir.pub/root/test-commit.git
cd test-commit
touch test.md
git add ./*
git commit -m "test"
git push # 可以看到下面输出 脚本已生效。
Enumerating objects: 3, done.
Counting objects: 100% (3/3), done.
Writing objects: 100% (3/3), 205 bytes | 205.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0)
remote: ERROR:
remote: ERROR: Your push was rejected because the commit
remote: ERROR: 38bcec39ffd4132c495a22af0ed1485fbe8852f4 in master
remote: ERROR: is missing the JIRA Issue. Example: 'DT-123 This is a commit example.'
remote: ERROR:
remote: ERROR: Please fix the commit message and push again.
remote: ERROR: https://help.github.com/en/articles/changing-a-commit-message
remote: ERROR
To http://gitlab.treesir.pub/root/test-commit.git
 ! [remote rejected] master -> master (pre-receive hook declined)
error: failed to push some refs to 'http://gitlab.treesir.pub/root/test-commit.git
  | 
 
如果项目一多了话,一个一个的添加还是有些麻烦,可以使用 gitlab 提供的 api 功能 进行一键查询对应的 hash path,并添加对应的脚本,不过需要我们有一定的 脚本编写能力。
其他钩子
限制上传文件的类型
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
  | 
cat custom_hooks/pre-receive.d/block-files 
#!/usr/bin/env bash
#
# Pre-receive hook that will block any new commits that contain files ending
# with .gz, .zip or .tgz
#
# More details on pre-receive hooks and how to apply them can be found on
# https://help.github.com/enterprise/admin/guides/developer-workflow/managing-pre-receive-hooks-on-the-github-enterprise-appliance/
#
zero_commit="0000000000000000000000000000000000000000"
# Do not traverse over commits that are already in the repository
# (e.g. in a different branch)
# This prevents funny errors if pre-receive hooks got enabled after some
# commits got already in and then somebody tries to create a new branch
# If this is unwanted behavior, just set the variable to empty
excludeExisting="--not --all"
while read oldrev newrev refname; do
  # echo "payload"
  echo $refname $oldrev $newrev
  # branch or tag get deleted
  if [ "$newrev" = "$zero_commit" ]; then
    continue
  fi
  # Check for new branch or tag
  if [ "$oldrev" = "$zero_commit" ]; then
    span=`git rev-list $newrev $excludeExisting`
  else
    span=`git rev-list $oldrev..$newrev $excludeExisting`
  fi
  for COMMIT in $span;
  do
    for FILE  in `git log -1 --name-only --pretty=format:'' $COMMIT`;
    do
      case $FILE in
      *.zip|*.gz|*.tgz|*.exe )
        echo "Hello there! We have restricted committing that filetype(*.zip|*.gz|*.tgz|*.exe). Please see Dave in IT to discuss alternatives."
        exit 1
        ;;
      esac
    done
  done
done
exit 0
  | 
 
此脚本中对 .zip、.gz、.tgz、.exe 类型的文件做了 block 处理。