GitLab CE 团队卡不住 commit 规范?我用一个全局 Hook 把整个公司拦下了

图片[1]-GitLab CE 团队卡不住 commit 规范?我用一个全局 Hook 把整个公司拦下了 - 爱三味-爱三味

很多团队使用 GitLab CE 时,希望强制执行 commit message 和分支命名规范,但 Push Rules 属于企业版功能,而 CI 校验往往拦截得太晚。本文介绍服务端 Hook 的适用原因,并提供一套已在 Docker 部署的 GitLab 18.x 上验证过的全局拦截方案,包括路径配置和 Gitaly 配置。

开场

先看一个常见场景。团队使用 GitLab CE 社区版,仓库里随处可见 update、fix bug、init、123、修改 之类的 commit,分支名也很随意,比如 test、wjw-dev、新分支2。想启用 Push Rules,才发现这个功能只支持 Premium 及以上版本;改用 CI 校验,通常又要等流水线运行后才能发现问题。此时代码已经推到远程仓库,再修改 commit history 会很麻烦。

这类问题的难点不在正则怎么写,而在于校验应该放在哪一层。本文会介绍三种适用于 GitLab CE 的代码规范落地方案,并给出一套已在 Docker 部署的 GitLab 18.x 上验证通过的全局服务端 Hook 配置,同时说明路径和 Gitaly 配置中容易出错的地方。

结论

  • 使用 GitLab 企业版:直接启用 Push Rules,在 Web 界面填写正则表达式,无需额外配置。
  • 使用 GitLab CE 社区版且有宿主机权限:在服务端配置 pre-receive Hook,在 push 时拦截不合规内容,避免其进入仓库。
  • 只能操作项目仓库、没有服务器权限:使用 `.gitlab-ci.yml` 配合 MR 校验。

配置时最容易出错的是路径。GitLab 15 及更高版本的钩子目录由 Gitaly 管理,旧教程提到的 /var/opt/gitlab/git-data/repositories/@hashed/... 或 gitlab-shell/custom_hooks 已不再生效。现在应使用 Gitaly 的 custom_hooks_dir,并在 gitlab.rb 中显式启用。下文说明具体配置方法。

图片[2]-GitLab CE 团队卡不住 commit 规范?我用一个全局 Hook 把整个公司拦下了 - 爱三味-爱三味

规范由来

团队规范不能凭空制定,业界已有两套较成熟的标准可供参考。

Commit Message 采用 Angular 规范,基本格式为 <type>(<scope>): <subject>。type 通常限定为七种:feat、fix、docs、style、refactor、test、chore。scope 可选,一般填写模块名,例如 feat(auth): add sso login。统一格式后,可以借助 standard-version 或 semantic-release 自动生成 CHANGELOG 和版本号。

分支命名参考 Git Flow,前缀统一为五种:feature/、bugfix/、hotfix/、release/、chore/。通过分支名,可以大致判断分支的生命周期和合并策略。hotfix/ 从 main 拉出,修复后合回 main 和 develop;feature/ 从 develop 拉出,完成后合回 develop。命名规则固定后,CI 可以根据前缀执行不同的流水线,例如只允许 release/* 触发预发部署。

对应的正则如下:●●●REGEX

# commit
^(feat|fix|docs|style|refactor|test|chore)(\(.+\))?: .+

# branch
^(main|master|develop|feature\/.*|feat\/.*|bugfix\/.*|fix\/.*|hotfix\/.*|release\/.*|chore\/.*)$

后续方案都围绕这两条正则展开。

三种方案

把三种拦截方案放在一起对比,差别会更清楚,也不容易选错。

方案拦截时机CE 是否可用是否消耗 CI代码是否进入远端运维成本
Push Rulespush 时否,仅企业版可用不消耗未进入极低,通过 Web 配置
.gitlab-ci.ymlpush 后运行流水线可用消耗已进入低
pre-receive Hookpush 时可用不消耗未进入中,需要宿主机权限

CI 校验的问题在于,代码推到远端后才会报错。比如向 test-branch push 了一个 init,随后流水线失败。这时只能通过 rebase 修改 commit history 后强推,或者删除分支重来。团队成员如果不熟悉 Git,处理过程中很容易把仓库弄乱。

服务端 Hook 在 git push 的握手阶段运行。脚本执行 exit 1 后,push 会直接失败,远端仓库不会写入代码。它使用原生 Git 机制,也不占用 GitLab Runner 资源,但需要有权限修改宿主机的挂载目录。团队制定相关规范时,可以用这份对比作为选型依据。

图片[3]-GitLab CE 团队卡不住 commit 规范?我用一个全局 Hook 把整个公司拦下了 - 爱三味-爱三味

脚本核心

服务端 Hook 按以下方式执行:GitLab 收到 push 后,会通过标准输入逐行传入 <oldrev> <newrev> <refname>。脚本以 exit 0 正常退出时放行,返回非零值时拒绝推送。脚本通过 echo 写入 stdout 的内容,会原样显示在推送者的终端中,可以用来说明拒绝原因。

下面是生产环境实际运行的版本,主要逻辑已添加注释:●●●BASH

#!/usr/bin/env bash

COMMIT_REGEX="^(feat|fix|docs|style|refactor|test|chore)(\(.+\))?: .+"
BRANCH_REGEX="^(main|master|develop|feature\/.*|feat\/.*|bugfix\/.*|fix\/.*|hotfix\/.*|release\/.*|chore\/.*)$"

# 全 0 的 SHA 代表分支删除(newrev)或新建分支(oldrev),要单独处理
zero_commit="0000000000000000000000000000000000000000"

while read -r oldrev newrev refname; do
    # 分支删除操作不拦截,只校验新建和更新
    if [ "$newrev" != "$zero_commit" ] && [[ "$refname" == refs/heads/* ]]; then
        branch_name="${refname#refs/heads/}"
        if [[ ! "$branch_name" =~ $BRANCH_REGEX ]]; then
            echo "❌ [GitLab 拦截] 分支名 '$branch_name' 不符合规范"
            exit 1
        fi
    fi

    # 新分支(oldrev 全 0)要用 --not --branches 找出该分支独有的 commit
    # 已有分支只需要检查增量部分,避免把历史遗留的脏 commit 也拦下来
    if [ "$oldrev" = "$zero_commit" ]; then
        commit_list=$(git rev-list "$newrev" --not --branches --not --tags)
    else
        commit_list=$(git rev-list "$oldrev..$newrev")
    fi

    for commit in $commit_list; do
        # 只校验第一行,去掉首尾空白,避免复制粘贴带空格误判
        commit_title=$(git log --format=%B -n 1 "$commit" | head -n 1 | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')
        if [[ ! "$commit_title" =~ $COMMIT_REGEX ]]; then
            echo "❌ [GitLab 拦截] Commit 格式错误: '$commit_title'"
            echo "正确示例: feat: add login 或 fix(order): fix crash"
            exit 1
        fi
    done
done

exit 0

这里有两个容易出错的地方。第一个是新分支的 commit 边界。如果不加 --not --branches --not --tags,git rev-list <newrev> 会列出从该分支向前追溯到的全部 commit,连仓库早期的提交也会被重新校验,最终出现大量报错。

第二个是 subject 的清洗。有些 IDE 会在 commit message 开头加入空格或 BOM,导致正则匹配失败,因此需要用 sed 去除首尾空白。

图片[4]-GitLab CE 团队卡不住 commit 规范?我用一个全局 Hook 把整个公司拦下了 - 爱三味-爱三味

踩坑一:路径变了

脚本不难写,麻烦的是让 GitLab 真正调用它。第一次部署后,init 仍然正常 push,脚本没有任何反应。

先查路径。网上搜到的基本都是 custom_hooks/pre-receive.d/,但完整路径有三种写法:

  • 老版本:`/var/opt/gitlab/git-data/repositories/<项目>.git/custom_hooks/`
  • 中间版本:`/opt/gitlab/embedded/service/gitlab-shell/custom_hooks/pre-receive.d/`
  • 新版本(15+ 到 18.x):Hook 由 Gitaly 接管,需要使用 `gitaly` 的 `custom_hooks_dir`

一开始按第二种方式,把脚本放在 gitlab-shell 下,结果没有生效。GitLab 15 之后,gitlab-shell 只处理 SSH 层,push 时实际执行 Hook 的是 Gitaly,老路径也不会再被读取。

项目级 Hook 可以临时解决问题,但项目在磁盘中使用哈希路径,例如 @hashed/d4/73/d4735e3a...git。每个新项目的哈希都不同,靠手工挂载很难维护,所以最后还是要使用全局路径。

踩坑二:空文件与静默通过

路径改对后,脚本还是没有生效。继续排查时,下面这条命令暴露了问题:●●●BASH

docker exec -it gitlab ls -la /var/opt/gitlab/gitaly/custom_hooks/pre-receive.d/
# -rwxr-xr-x 1 root root  0 Jul 17 16:03 check_rules

文件大小竟然是 0 字节。之前用 IDE 远程编辑时操作失败,文件被覆盖成了空文件。空脚本执行后,退出码依然是 0,GitLab 便把它当成校验通过,直接放行。整个过程看起来都没问题:文件存在,权限正常,路径也正确,很难第一时间发现异常。

如果线上遇到「配置已经生效,功能却没反应」的情况,可以按这个顺序检查:先看文件是否为空,再查权限和归属,最后检查脚本逻辑。顺序错了,很容易在无关的问题上浪费时间。

还有一个常被忽略的问题:文件所有者。GitLab 容器内使用 git 用户运行,UID 通常是 998。如果宿主机上的文件由 root 写入,容器中的 git 用户可能无法执行。写入文件后,最好再执行:●●●BASH

docker exec -it -u 0 gitlab chown -R git:git /var/opt/gitlab/gitaly/custom_hooks
docker exec -it -u 0 gitlab chmod -R 755 /var/opt/gitlab/gitaly/custom_hooks
图片[5]-GitLab CE 团队卡不住 commit 规范?我用一个全局 Hook 把整个公司拦下了 - 爱三味-爱三味

踩坑三:Gitaly 默认不认这个路径

问题就出在这里。路径没错,文件不为空,权限也正常,但 init 仍然可以提交。查看日志:●●●BASH

docker exec -it gitlab tail -f /var/log/gitlab/gitaly/current

日志里没有任何 custom_hooks 或 pre-receive 的调用记录,说明 Gitaly 根本没有从这个目录读取脚本。

查阅官方文档后发现,新版 GitLab 需要在 gitlab.rb 中显式配置 gitaly 的 custom_hooks_dir。它的默认值可能为空,也可能指向其他位置。只把文件放进 /var/opt/gitlab/gitaly/custom_hooks 不会生效,因为 Gitaly 默认不读取这个路径。

在挂载出来的 ./config/gitlab.rb 中加入:●●●RUBY

gitaly['custom_hooks_dir'] = "/var/opt/gitlab/gitaly/custom_hooks"

然后执行:●●●BASH

docker exec -it gitlab gitlab-ctl reconfigure

这条命令会重新渲染 Gitaly 的 config.toml,写入 [hooks] custom_hooks_dir 配置。reconfigure 完成后不用重启容器,Gitaly 会自动重载。

此时再次 push 一个 init,终端会直接显示红色的拦截提示。整个链路终于跑通了。

全局配置方案

把前面遇到的问题整理后,可以按下面的步骤统一配置。设置完成后,现有项目和之后创建的新项目都会生效。

第一步:修改 gitlab.rb,让 Gitaly 识别路径

在挂载的 ./config/gitlab.rb 文件末尾加入:●●●RUBY

gitaly['custom_hooks_dir'] = "/var/opt/gitlab/gitaly/custom_hooks"

第二步:在宿主机上准备脚本●●●BASH

mkdir -p ./data/gitaly/custom_hooks/pre-receive.d

sudo tee ./data/gitaly/custom_hooks/pre-receive.d/check_rules >/dev/null <<'EOF'
# 这里贴入前面那份完整脚本
EOF

<<'EOF' 外的单引号不能省略,否则 shell 会提前展开 $oldrev 等变量。

第三步:修正权限●●●BASH

docker exec -it -u 0 gitlab chown -R git:git /var/opt/gitlab/gitaly/custom_hooks
docker exec -it -u 0 gitlab chmod -R 755 /var/opt/gitlab/gitaly/custom_hooks

这一步必须执行。

第四步:执行 reconfigure,使配置生效●●●BASH

docker exec -it gitlab gitlab-ctl reconfigure

第五步:验证配置

在本地任选一个项目执行:●●●BASH

git commit -m "init" --allow-empty
git push origin main
# 应该看到 remote: ❌ [GitLab 拦截] ...

如果看到红色的拒绝提示,说明配置已经生效。

图片[6]-GitLab CE 团队卡不住 commit 规范?我用一个全局 Hook 把整个公司拦下了 - 爱三味-爱三味

边界与取舍

这套方案并非适用于所有情况,有几个边界需要提前说明。

不适合使用服务端 Hook 的场景:

  • GitLab.com 或 SaaS 版:无法获取宿主机权限,只能使用 Push Rules 或 CI。
  • 团队有大量遗留仓库,或历史 commit 不符合规范:Hook 主要校验新增内容。rebase 老分支时,旧 commit 可能因不合规而被拦截。遇到这种情况,可以为老分支设置白名单,或临时禁用 Hook,集中完成迁移。
  • 不同项目需要使用不同规则:全局 Hook 会统一执行同一套规则。如果 A 项目要求中文 commit,B 项目要求英文,更适合使用项目级 `custom_hooks`。

推给团队之前,可以先做好两件事:

  1. 配置 husky 和 commitlint,先在本地拦截。开发者执行 `git commit` 时就能发现问题,不必等到 push 后再被服务端拒绝。本地和服务端各设一道检查,使用起来更顺手。
  2. 写清楚豁免机制。谁有权临时关闭 Hook,出现问题该联系谁,都要提前约定,避免规范影响正常开发。

最后可以检查一下项目的 commit 历史。很多团队嘴上说有规范,打开 log 却满是 update、修改一下、test。这样的规范其实只留在 wiki 里。

上线检查清单

部署前,按下面的项目逐项检查:

  • [ ] `gitlab.rb` 中已加入 `gitaly[‘custom_hooks_dir’]`,并执行过 `reconfigure`
  • [ ] 脚本不是空文件,可用 `ls -la` 检查 size 列。空文件这个坑很容易忽略
  • [ ] 权限已设为 755,文件归属为 `git:git`
  • [ ] 脚本第一行为 `#!/usr/bin/env bash`,不要写成 `#!/bin/sh`,两者的正则语法存在差异
  • [ ] 已排除删除分支的操作,即 newrev 全为 0 的情况,避免误拦截 `git push –delete`
  • [ ] 新分支使用 `–not –branches –not –tags` 限定增量,避免误判历史 commit
  • [ ] 选择一个真实项目,push `init` 做冒烟测试;出现红字 `remote rejected`,说明拦截已经生效
  • [ ] 团队成员已了解豁免流程和对应联系人

遇到问题时,按以下顺序排查:

  1. 查看 Gitaly 日志,确认是否触发了 Hook
  2. 检查脚本文件大小和权限
  3. 进入容器,手动向脚本传入 stdin:`echo “oldsha newsha refs/heads/test” | ./check_rules`
  4. 对比 `gitlab.rb` 配置和 Gitaly 实际加载的 `config.toml`,确认两者一致

技术文章最怕只讲方案不谈代价,只写成功经验不提踩过的坑。本文会尽量讲清 GitLab CE 落地规范过程中可能遇到的问题,方便需要时查阅。团队里负责 DevOps 或代码规范的人也可以直接参考,能少走不少弯路。如果你在其他 GitLab 版本中遇到过更棘手的问题,尤其是“配置全对却不生效”这类情况,可以补充具体场景。

© 版权声明
THE END
喜欢就支持一下吧
点赞15 分享