一、问题背景:从SVN到Git的混乱期
2022年Q3,我们团队从SVN整体迁移到Git。最初三个月,团队只是把Git当成“能离线提交的SVN”,所有人继续在master分支上直接提交,冲突频发。最离谱的一周,master分支出现了11次强制推送,导致同事本地仓库直接报废。
更痛的是代码审查形同虚设。SVN时代我们用“提交后邮件通知”,迁移到Git后变成“提交后群里喊一声”。有一次一个同事把数据库密码硬编码提交到了master,直到三天后安全扫描才被发现。
这促使我们重新设计工作流。本文记录的就是这套最终落地的方案,以及背后的思考过程。
二、环境与版本:我们用的具体工具链
- Git版本:2.39.1(服务端通过GitLab 15.11.2托管)
- Code Review工具:Gerrit 3.7(基于SSH的refs/for/魔法分支)
- CI/CD:Jenkins 2.414.2 + SonarQube 9.9 LTS
- 制品仓库:Nexus 3.49
- 部署目标:Kubernetes 1.27(三个环境:dev/staging/prod)
选择Gerrit而非GitLab MR的原因是:Gerrit原生支持“提交即审查”,每个提交在合入前必须经过+2评分,且强制rebase。这对保证线性历史有奇效。
三、方案设计:折中后的分支模型
我们最终采用的是Git Flow的简化版 + 单master长期分支的组合:
master(受保护,禁止直接push)
├── feature/xxx-{issueId}-{描述} # 功能分支,来源master
├── release/{version} # 发布分支,来源master,合回master+develop
└── hotfix/{issueId}-{描述} # 热修分支,来源master,合回master+develop
关键决策点:
- 砍掉了develop分支:团队规模25人,三个Scrum团队并行开发,develop分支的同步成本远大于收益。我们让所有feature分支直接从master拉出,通过Gerrit的rebase策略保证合入时master始终领先。
- 强制rebase而非merge:Gerrit配置了
--rebase策略,合入时自动变基,保证master历史上是绝对线性的。配合git log --first-parent查看主线条目一目了然。 - 提交信息规范:通过
commit-msghook强制添加Change-Id(Gerrit要求),同时我们自定义了前缀校验。
四、核心实现:分支策略与Hook脚本
4.1 分支命名与保护规则
在GitLab中设置master分支为protected,同时添加以下规则(.gitlab/group/protected_branches.rb):
# 只允许Maintainer角色force push(紧急回滚时用)
protect_branch 'master' do
allowed_to_push(user: :maintainer)
allow_force_push(true) # 仅限紧急情况
code_owner_approval_required(true)
end
4.2 commit-msg Hook(强制Change-Id与规范前缀)
我们在服务端克隆模板中放置了.git/hooks/commit-msg,内容精简如下:
#!/bin/sh
# 自动追加Change-Id(Gerrit必需)
CHANGE_ID_MSG="Change-Id: I$(cat /proc/sys/kernel/random/uuid | tr -d '-')"
if ! grep -q "Change-Id:" "$1"; then
echo "$CHANGE_ID_MSG" >> "$1"
fi
# 校验提交信息格式:type(scope): description
COMMIT_MSG=$(cat "$1")
case "$COMMIT_MSG" in
feat:*|fix:*|docs:*|refactor:*|test:*|chore:*)
;;
*)
echo "ERROR: 提交信息必须以feat/fix/docs/refactor/test/chore开头" >&2
exit 1
;;
esac
4.3 Core Review流程:Gerrit双人+2规则
我们配置了Gerrit的project.config:
[access "refs/heads/*"]
label-Code-Review = -2..+2 group Developers
label-Code-Review = -2..+2 group Maintainers
label-Verified = -1..+1 group CI-Bot
[submit]
mergeContent = false
action = rebase if necessary
提交者本人不能给自己+2,且必须至少两名Maintainer给+2才能合入。Verified标签由Jenkins CI自动打+1,如果SonarQube质量门禁失败则打-1直接阻塞合入。
五、CI/CD集成:Jenkinsfile与SonarQube门禁
5.1 Jenkins多分支流水线配置
每个feature分支的PR触发流水线,核心阶段:编译(Maven)→ 单元测试(JUnit)→ SonarQube分析 → 构建Docker镜像 → 部署到dev环境。
// Jenkinsfile(片段)
pipeline {
agent { label 'maven-builder' }
triggers {
gerrit trigger(
serverName: 'gerrit',
gerritProjects: [[
compareType: 'PLAIN',
pattern: 'platform/core',
branches: [[compareType: 'REG_EXP', pattern: 'master|feature/.*']]
]]
)
}
stages {
stage('Unit Test') {
steps {
sh 'mvn test -DskipITs'
junit '**/target/surefire-reports/*.xml'
}
}
stage('Code Quality') {
steps {
withSonarQubeEnv('sonar-9.9') {
sh 'mvn sonar:sonar -Dsonar.qualitygate.wait=true'
}
}
}
stage('Build Image') {
steps {
sh 'docker build -t registry.internal/app:${GIT_COMMIT} .'
}
}
stage('Deploy Dev') {
when { branch 'feature/*' }
steps {
sh 'kubectl set image deployment/app app=registry.internal/app:${GIT_COMMIT} -n dev'
}
}
}
post {
success {
gerrit review: 'labels: {Verified=+1}'
}
failure {
gerrit review: 'labels: {Verified=-1}'
}
}
}
5.2 SonarQube质量门禁硬性指标
我们设定了以下阈值,不满足直接拒绝合入:
- 新增代码覆盖率 ≥ 80%
- 阻断级(Blocker)Bug = 0
- 新增代码的复杂度 ≥ 15则告警
实际运行半年后,整体代码覆盖率从34%提升到61%,线上缺陷密度从每千行2.1个降至0.7个。
六、踩坑与优化:三个真实教训
6.1 教训一:Gerrit的rebase策略与本地冲突
最早期我们配置了mergeContent = false,导致Gerrit不允许rebase。后来改为true后,又出现了“提交者本地分支落后于master”的问题。解决方案是强制推行本地rebase工作流:
git fetch origin master
git rebase origin/master
git push origin HEAD:refs/for/master
我们在团队wiki里写了脚本,但真正解决问题的是在Gerrit的comment中自动添加提示,当提交落后时Gerrit会评论“Cannot merge due to conflict”。
6.2 教训二:CI构建时间过长
初始Jenkins配置在master分支上每次提交都跑全量测试,耗时45分钟。后来我们引入maven incremental build + git diff --name-only来检测变更模块,只构建受影响的服务。优化后feature分支平均构建时间降到12分钟,master合入门禁(全量测试+安全扫描)控制在28分钟。
6.3 教训三:热修流程被滥用
最初hotfix分支也走Gerrit双+2,导致紧急线上问题等审查等了4小时。我们后来增加了一条“hotfix快速通道”:允许一名Maintainer+1且CI Verified+1即可合入,但要求24小时内必须补开Review记录。这个策略上线后,热修平均合入时间从2.5小时降至25分钟。
七、效果数据:迁移一年后的对比
| 指标 | SVN时代(2021) | Git Flow+Gerrit(2023) |
|---|---|---|
| 部署频率 | 每周2次 | 每日15次(峰值30次) |
| 线上故障回滚时间 | 40分钟(重新部署) | 3分钟(git revert + CI触发) |
| master分支冲突次数/周 | 无法统计(常态冲突) | 0(强制rebase保证) |
| 代码Review覆盖率 | 0% | 100%(强制+2) |
| 新增Bug密度(千行) | 2.1 | 0.7 |
总结:Git工作流的核心不是工具,而是“可强制”的规则。如果你团队还在靠“自觉”做代码审查,请立刻上Gerrit或GitLab的approval_rules。另外,不要迷信某个标准模型——Git Flow、GitHub Flow、Trunk-Based都试过,最终找到适合自己团队规模和发布节奏的折中方案才是关键。我们的下一阶段目标是引入trunk-based的短分支模式,配合feature flag,预计部署频率还能再翻一番。