以下是分模块实现的完整代码方案,包含 配置管理、日志模块、校验逻辑 和 主程序,采用工程化结构设计:
目录结构
/opt/gitlab/custom_hooks/
├── config.sh # 集中配置
├── pre-receive # 主入口脚本
├── modules/
│ ├── log.sh # 日志模块
│ ├── validator.sh # 校验核心逻辑
│ └── utils.sh # 工具函数
└── logs/
└── hook_audit.log # 审计日志
模块 1: 配置文件 (config.sh)
#!/bin/bash
# 全局配置参数
readonly MAX_FILE_SIZE=104857600 # 100MB
readonly COMMIT_MSG_REGEX='^[A-Z]+-[0-9]+: ' # 提交信息规则
readonly MAX_COMMITS_PROCESS=500 # 单次推送最多处理500个提交(防溢出)
# 文件白名单配置
readonly WHITELIST_PATTERNS=(
"*.pdf" # 允许所有PDF文件
"vendor/*" # 允许vendor目录下所有文件
"lib/*.so" # 允许lib目录下的.so文件
)
# 日志配置
readonly LOG_ENABLED=true
readonly LOG_FILE="/opt/gitlab/custom_hooks/logs/hook_audit.log"
模块 2: 日志模块 (modules/log.sh)
#!/bin/bash
# 导入配置
source /opt/gitlab/custom_hooks/config.sh
# 日志记录函数
log_rejection() {
local commit_hash="$1"
local reason="$2"
local file_path="${3:-N/A}"
if [ "$LOG_ENABLED" = true ]; then
echo "[$(date '+%Y-%m-%d %H:%M:%S')] REJECTED - Commit: ${commit_hash} - File: ${file_path} - Reason: ${reason}" >> "$LOG_FILE"
fi
}
# 错误格式化输出
print_error() {
local commit_hash="$1"
local reason="$2"
local details="$3"
echo "==================================================================="
echo "[REJECTED] 提交 ${commit_hash:0:8} 未通过校验"
echo "原因: ${reason}"
echo "详情: ${details}"
echo "==================================================================="
}
模块 3: 校验逻辑 (modules/validator.sh)
#!/bin/bash
# 导入依赖
source /opt/gitlab/custom_hooks/config.sh
source /opt/gitlab/custom_hooks/modules/log.sh
source /opt/gitlab/custom_hooks/modules/utils.sh
# 提交信息校验
validate_commit_message() {
local commit="$1"
local msg=$(git log --format=%B -n 1 "$commit")
if ! [[ "$msg" =~ $COMMIT_MSG_REGEX ]]; then
print_error "$commit" "提交信息格式错误" "要求格式: ${COMMIT_MSG_REGEX}\n实际内容: ${msg}"
log_rejection "$commit" "Invalid commit message"
return 1
fi
return 0
}
# 文件白名单检查
is_whitelisted() {
local file="$1"
for pattern in "${WHITELIST_PATTERNS[@]}"; do
if [[ "$file" == $pattern ]]; then
return 0
fi
done
return 1
}
# 文件大小校验
validate_file_size() {
local commit="$1"
# 使用 null 分隔符处理特殊文件名
git diff-tree -z --no-commit-id --name-only -r "$commit" | while IFS= read -r -d '' file; do
[ -z "$file" ] && continue
# 白名单跳过
if is_whitelisted "$file"; then
echo "[INFO] 跳过白名单文件: $file"
continue
fi
# 获取文件大小
file_size=$(git cat-file -s "$commit:$file" 2>/dev/null)
[ $? -ne 0 ] && continue # 忽略已删除文件
if [ "$file_size" -gt "$MAX_FILE_SIZE" ]; then
print_error "$commit" "文件大小超限" "文件: ${file}\n限制: $((MAX_FILE_SIZE/1024/1024))MB, 实际: $((file_size/1024/1024))MB"
log_rejection "$commit" "File size exceeded" "$file"
return 1
fi
done
return 0
}
模块 4: 工具函数 (modules/utils.sh)
#!/bin/bash
# 安全获取提交列表
get_safe_commit_list() {
local oldrev="$1"
local newrev="$2"
# 限制处理的提交数量防止内存溢出
git rev-list --max-count=$MAX_COMMITS_PROCESS "$oldrev".."$newrev"
}
# 检查是否为新分支创建
is_new_branch() {
[ "$oldrev" = "0000000000000000000000000000000000000000" ]
}
主入口脚本 (pre-receive)
#!/bin/bash
# 初始化环境
HOOKS_DIR="$(dirname "$0")"
source "$HOOKS_DIR/config.sh"
source "$HOOKS_DIR/modules/utils.sh"
source "$HOOKS_DIR/modules/validator.sh"
source "$HOOKS_DIR/modules/log.sh"
# 主处理流程
while read -r oldrev newrev refname; do
# 跳过空新修订(删除分支)
[ -z "$newrev" ] && continue
# 处理新分支创建场景
is_new_branch && oldrev="${newrev}^"
# 获取安全提交列表
commits=$(get_safe_commit_list "$oldrev" "$newrev")
# 遍历每个提交
for commit in $commits; do
validate_commit_message "$commit" || exit 1
validate_file_size "$commit" || exit 1
done
done
exit 0
部署步骤
-
创建目录结构:
sudo mkdir -p /opt/gitlab/custom_hooks/{modules,logs} sudo chown -R git:git /opt/gitlab/custom_hooks -
设置权限:
sudo chmod -R 755 /opt/gitlab/custom_hooks sudo chmod 644 /opt/gitlab/custom_hooks/logs/hook_audit.log -
链接到仓库:
# 进入目标仓库的 hooks 目录 cd /var/opt/gitlab/git-data/repositories/<group>/<project>.git/custom_hooks/ # 创建软链接 ln -s /opt/gitlab/custom_hooks/pre-receive .
验证测试
测试用例 1:提交信息违规
git commit --allow-empty -m "invalid message"
git push origin main
# 预期输出:
# [REJECTED] 提交 xxxxxxxx 未通过校验
# 原因: 提交信息格式错误
测试用例 2:文件大小超限
dd if=/dev/zero of=oversize_file bs=1M count=101
git add oversize_file
git commit -m "TASK-001: Add large file"
git push origin main
# 预期输出:
# [REJECTED] 提交 xxxxxxxx 未通过校验
# 原因: 文件大小超限
remote: -------------------------------------------------------------------
remote: [REJECTED] 提交 7e624af8 包含超大文件: oversize_file
remote: 大小限制: 100MB, 实际大小: 110MB
remote: -------------------------------------------------------------------
To ssh://<gitlab-url>/test/test.git
! [remote rejected] main -> main (pre-receive hook declined)
error: failed to push some refs to 'ssh://<gitlab-url>/test/test.git'
检查日志
tail -f /opt/gitlab/custom_hooks/logs/hook_audit.log
# 输出示例:
# [2023-10-05 14:30:00] REJECTED - Commit: a1b2c3d4 - File: oversize_file - Reason: File size exceeded
此设计实现了以下关键特性:
- 模块解耦:各功能独立成模块,修改校验规则无需改动主流程
-
安全防护:
- 限制处理的最大提交数量 (
MAX_COMMITS_PROCESS) - 使用 null 分隔符处理特殊文件名
- 限制处理的最大提交数量 (
- 审计追踪:详细日志记录所有拒绝操作
- 灵活配置:白名单机制和阈值参数集中管理
如需扩展功能(如添加新的校验规则),只需创建新模块并在主流程中调用即可。这种架构特别适合需要统一管理多个 GitLab 仓库的企业环境。







网友评论