上一篇文章《后端编码规范系列(二十一)使用checkstyle+git的hook,实现提交代码前完成代码规范的整改》我们实现了在本地进行 pre-commit 的钩子校验,但是在实际情况下,pre-commit 的钩子没法实现团队共享,需要大家手动的去添加这个钩子,导致的情况是,大家也能知道,人都是懒得,所以有没有更好的办法来解决呢?其实是有的就是使用 gitlab 的服务器端钩子,在服务端利用 pre-commit 来实现这里的钩子。下面我们来演示一下:
1、安装 gitlab
首先我们需要在服务器端安装一份 gitlab,这里我们主要是演示,所以这里我们使用docker 来部署 gitlab。具体可参考《docker 部署 gitlab》
2、创建钩子目录
这里我们使用的宿主机和 docker 里面的路径映射以上诉 1 安装的映射为准,所以这里我们很多东西可以直接在宿主机上操作,比如现在我们需要创建一个钩子目录,那么我们在宿主机上执行:
# 在 GitLab 容器内执行,或在宿主机上执行(因为路径已挂载) mkdir -p /srv/gitlab/data/gitaly/custom_hooks/pre-receive.d
3、安装 jdk
这里我们使用代码 check-style 的话,主要是只想 java 命令,所以这里需要一个 java 环境,目前我们 check-style 的jar 包是支持 jdk8 的,所以这里我们需要进入到容器中安装 jdk8 的环境
#进入docker容器 docker exec -it gitlab /bin/bash # 在容器内执行更新并安装 JDK(以 Debian/Ubuntu 基础镜像为例) apt-get update && apt-get install -y openjdk-8-jdk # 验证java版本 java -version
4、上传checkstyle 的 jar 文件和 xml 文件
这里我们把前面准备的 chekstyle 的 jar 文件和 xml 文件上传到服务器的目录里面去
#在宿主机上执行 mkdir -p /srv/gitlab/data/git-hooks/tools #将 checkstyle-9.3-all.jar 和 google-check-style.xml 从本地拷贝到 /srv/gitlab/data/git-hooks/tools/ 下
5、创建执行脚本
接下来我们就要创建我们的钩子脚本了
# 在宿主机上执行 mkdir -p /srv/gitlab/data/gitaly/custom_hooks/pre-receive.d #在宿主机上执行 touch /srv/gitlab/data/gitaly/custom_hooks/pre-receive.d/code-style-check.sh #然后把下面的内容拷贝过去
#!/bin/bash
# 服务器端 pre-receive 钩子 - 代码规范检查
# 日志配置
LOG_LEVEL=${LOG_LEVEL:-"INFO"} # 支持 DEBUG, INFO, WARN, ERROR
LOG_FILE="${LOG_FILE:-"/var/log/gitlab/hooks/pre-receive.log"}"
HOOK_NAME="$(basename "$0")"
# 创建日志目录
mkdir -p "$(dirname "$LOG_FILE")"
# 日志函数
log() {
local level=$1
shift
local message=$*
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
# 定义日志级别数值
local level_num=0
case "$level" in
"DEBUG") level_num=1 ;;
"INFO") level_num=2 ;;
"WARN") level_num=3 ;;
"ERROR") level_num=4 ;;
*) level_num=2 ;;
esac
local config_level_num=2
case "$LOG_LEVEL" in
"DEBUG") config_level_num=1 ;;
"INFO") config_level_num=2 ;;
"WARN") config_level_num=3 ;;
"ERROR") config_level_num=4 ;;
*) config_level_num=2 ;;
esac
# 只有当前级别 >= 配置级别时才记录
if [ $level_num -ge $config_level_num ]; then
echo "${timestamp} [${level}] ${HOOK_NAME} - ${message}" | tee -a "$LOG_FILE"
fi
}
# 命令执行日志函数
log_command() {
local command="$*"
log "DEBUG" "执行命令: ${command}"
local start_time=$(date +%s)
# 执行命令并捕获输出和退出码
local output
output=$(eval "$command" 2>&1)
local exit_code=$?
local end_time=$(date +%s)
local duration=$((end_time - start_time))
log "DEBUG" "命令退出码: ${exit_code}, 执行时间: ${duration}秒"
if [ $exit_code -ne 0 ]; then
log "ERROR" "命令执行失败: ${command}"
if [ -n "$output" ]; then
log "DEBUG" "命令输出: ${output}"
fi
else
log "DEBUG" "命令执行成功"
if [ -n "$output" ] && [ "$LOG_LEVEL" = "DEBUG" ]; then
log "DEBUG" "命令输出: ${output}"
fi
fi
return $exit_code
}
# Checkstyle 错误信息翻译函数
translate_checkstyle_message() {
local message="$1"
# 替换常见的 Checkstyle 错误信息为中文
echo "$message" | sed \
-e "s/Extra separation in import group before/导入组中存在额外的空行分隔在/" \
-e "s/has incorrect indentation level/缩进级别不正确/" \
-e "s/expected level should be/期望的级别应为/" \
-e "s/'member def modifier'/'成员定义修饰符'/" \
-e "s/'method def modifier'/'方法定义修饰符'/" \
-e "s/'method def' child/'方法定义'子项/" \
-e "s/'import' should be separated from previous line/'import'语句应与前一行有空行分隔/" \
-e "s/Using the '.*' form of import should be avoided/应避免使用'.*'形式的导入/" \
-e "s/Missing a Javadoc comment/缺少Javadoc注释/" \
-e "s/'CLASS_DEF' should be separated from previous line/'类定义'应与前一行有空行分隔/" \
-e "s/';' is not followed by whitespace/';'后面没有空格/" \
-e "s/WhitespaceAround: '{' is not followed by whitespace. Empty blocks may only be represented as {} when not part of a multi-block statement/空格环绕: '{'后面没有空格。空块只能表示为{},除非是多块语句的一部分/" \
-e "s/File contains tab characters/文件包含制表符/" \
-e "s/Line is longer than/行长度超过/" \
-e "s/File does not end with a newline/文件没有以换行符结尾/" \
-e "s/Unused import/未使用的导入/" \
-e "s/Redundant import/冗余的导入/" \
-e "s/Avoid nested blocks/避免嵌套块/" \
-e "s/Variable/变量/" \
-e "s/not used/未使用/" \
-e "s/must be private/必须为private/" \
-e "s/must be final/必须为final/" \
-e "s/Expected @param tag for/期望有@param标签用于/" \
-e "s/Expected @return tag/期望有@return标签/" \
-e "s/Expected @throws tag for/期望有@throws标签用于/"
}
log "INFO" "开始代码规范检查..."
log "DEBUG" "脚本参数: $*"
log "DEBUG" "环境变量: LOG_LEVEL=${LOG_LEVEL}, LOG_FILE=${LOG_FILE}"
# 设置检查工具路径(根据服务器实际路径修改)
CHECKSTYLE_JAR="/var/opt/gitlab/git-hooks/tools/checkstyle-9.3-all.jar"
CHECKSTYLE_CONFIG="/var/opt/gitlab/git-hooks/tools/google-check-style.xml"
log "INFO" "检查工具配置:"
log "INFO" " - Checkstyle JAR: ${CHECKSTYLE_JAR}"
log "INFO" " - Checkstyle 配置: ${CHECKSTYLE_CONFIG}"
# 检查检查工具是否存在
if [ ! -f "$CHECKSTYLE_JAR" ]; then
log "ERROR" "Checkstyle jar文件不存在: $CHECKSTYLE_JAR"
exit 1
fi
if [ ! -f "$CHECKSTYLE_CONFIG" ]; then
log "ERROR" "Checkstyle配置文件不存在: $CHECKSTYLE_CONFIG"
exit 1
fi
log "INFO" "检查工具验证通过"
success=true
push_count=0
processed_refs=0
# 在 pre-receive 钩子中,通过标准输入获取推送的引用信息
while read -r oldrev newrev refname; do
push_count=$((push_count + 1))
log "INFO" "处理引用 [#${push_count}]: ${refname} (旧版本: ${oldrev} -> 新版本: ${newrev})"
# 如果新版本是全零哈希,表示是删除分支操作,跳过检查
if [ "$newrev" = "0000000000000000000000000000000000000000" ]; then
log "INFO" "检测到分支删除操作,跳过检查"
continue
fi
processed_refs=$((processed_refs + 1))
# 获取提交范围信息用于日志
if [ "$oldrev" = "0000000000000000000000000000000000000000" ]; then
range="所有提交(新分支)"
commit_count=$(git rev-list --count "$newrev" 2>/dev/null || echo "未知")
else
range="${oldrev:0:8}..${newrev:0:8}"
commit_count=$(git rev-list --count "$oldrev..$newrev" 2>/dev/null || echo "未知")
fi
log "INFO" "提交范围: ${range}, 提交数量: ${commit_count}"
# 如果是新分支创建(oldrev是全零哈希),检查所有文件
if [ "$oldrev" = "0000000000000000000000000000000000000000" ]; then
log "INFO" "检测到新分支创建,检查所有Java文件"
# 获取新分支中的所有Java文件
CHANGED_FILES=$(git ls-tree -r --name-only "$newrev" | grep -E '\.java$')
else
# 获取本次推送中新增或修改的Java文件列表
CHANGED_FILES=$(git diff --name-only --diff-filter=ACM "$oldrev" "$newrev" | grep -E '\.java$')
fi
if [ -z "$CHANGED_FILES" ]; then
log "INFO" "没有修改Java文件,跳过检查"
continue
fi
file_count=$(echo "$CHANGED_FILES" | wc -l)
log "INFO" "发现 ${file_count} 个Java文件变更:"
log "DEBUG" "变更文件列表:\n${CHANGED_FILES}"
# 创建临时目录用于文件检查
TEMP_DIR=$(mktemp -d)
trap 'rm -rf "$TEMP_DIR"; log "DEBUG" "清理临时目录: $TEMP_DIR"' EXIT
log "DEBUG" "创建临时目录: ${TEMP_DIR}"
checked_files=0
failed_files=0
# 遍历每个更改的文件进行检查
for file in $CHANGED_FILES; do
checked_files=$((checked_files + 1))
log "INFO" "检查文件 [${checked_files}/${file_count}]: ${file}"
# 将文件内容检出到临时位置进行检查
TEMP_FILE="$TEMP_DIR/$(basename "$file")"
mkdir -p "$(dirname "$TEMP_FILE")"
# 使用git show提取文件内容
log "DEBUG" "提取文件内容到临时文件: ${TEMP_FILE}"
if git show "$newrev:$file" > "$TEMP_FILE" 2>/dev/null; then
# 检查文件是否成功检出且不为空
if [ ! -s "$TEMP_FILE" ]; then
log "WARN" "文件为空或无法读取: $file,跳过检查"
continue
fi
file_size=$(wc -c < "$TEMP_FILE")
log "DEBUG" "文件提取成功,大小: ${file_size} 字节"
# 使用 Checkstyle 检查文件
log "DEBUG" "开始Checkstyle检查"
checkstyle_cmd="java -jar \"$CHECKSTYLE_JAR\" -c \"$CHECKSTYLE_CONFIG\" \"$TEMP_FILE\""
log "DEBUG" "执行命令: $checkstyle_cmd"
checkstyle_output=$(eval "$checkstyle_cmd" 2>&1)
exit_code=$?
log "DEBUG" "Checkstyle检查完成,退出码: ${exit_code}"
# 输出checkstyle结果(同时输出错误和警告信息)
if [ $exit_code -ne 0 ] || echo "$checkstyle_output" | grep -q -E "(ERROR|WARN)" ; then
log "ERROR" "文件检查失败: ${file}"
failed_files=$((failed_files + 1))
# 提取关键错误和警告信息并进行翻译
error_summary=$(echo "$checkstyle_output" | grep -E "(ERROR|WARN)" | head -10)
if [ -n "$error_summary" ]; then
log "ERROR" "检查结果摘要 (包含ERROR和WARN):"
echo "$error_summary" | while IFS= read -r line; do
# 翻译错误信息
translated_line=$(translate_checkstyle_message "$line")
# 根据错误级别使用不同的日志级别
if echo "$line" | grep -q "ERROR"; then
log "ERROR" " $translated_line"
elif echo "$line" | grep -q "WARN"; then
log "WARN" " $translated_line"
else
log "ERROR" " $translated_line"
fi
done
fi
if [ "$LOG_LEVEL" = "DEBUG" ]; then
log "DEBUG" "完整检查输出:\n${checkstyle_output}"
fi
success=false
else
log "INFO" "✓ 文件检查通过: ${file}"
fi
else
log "WARN" "无法检出文件: ${file},可能文件已被删除"
fi
done
log "INFO" "引用 ${refname} 检查完成: 通过 $((checked_files - failed_files))/${checked_files} 个文件"
done
log "INFO" "所有引用处理完成: 共处理 ${processed_refs}/${push_count} 个引用"
# 输出最终检查结果
if $success; then
log "INFO" "✓ 所有文件检查通过,允许推送"
exit 0
else
log "ERROR" "❌❌ 代码规范检查未通过!请根据上述信息修复代码后重新推送。"
log "WARN" "检查未通过,拒绝推送"
exit 1
fi接着进行如下操作:
#进入到 docker 容器 docker exec -it gitlab bash #进入到脚本目录 cd /var/opt/gitlab/gitaly/custom_hooks/pre-receive.d/ #授予执行权限 chmod +x code-style-check.sh #授予git用户 chown git:git code-style-check.sh
6、修改配置文件
这里我们要修改 git 的配置文件,让他加载我们 pre-recive 的目录,这里还是进入到 git 容器中
#进入docker容器 docker exec -it gitlab bash #修改配置文件(这里如果是使用docker 部署的话,在宿主机上对应的文件是:/srv/gitlab/config/gitlab.rb) vi /etc/gitlab/gitlab.rb
搜索custom_hooks_dir,可以看到最新版本的 git 配置文件这里是一个 json,所以需要把这里打开
然后这里的路径直接配置:
#docker 容器对应的路径是这个,如果不是 docker 的话,使用自己的路径就好。 /var/opt/gitlab/gitaly/custom_hooks
7、重启 gitlab
接着重启 gitlab 即可,就可以进行测试了。我们找个 test 的项目,提交不规范的代码:
就会直接被终止,提示:
进行了验证。验证不通过,不允许提交
备注:
1、这里可能会出现找不到文件的提示:/var/log/gitlab/hooks/pre-receive.log。临时的解决办法是:
#进入到docker容器 docker exec -it gitlab bash #创建文件夹 mkdir -p /var/log/gitlab/hooks #创建文件 touch /var/log/gitlab/hooks/pre-receive.log #授权 chown git:git /var/log/gitlab/hooks/pre-receive.log







还没有评论,来说两句吧...