一、问题背景:从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

关键决策点:

  1. 砍掉了develop分支:团队规模25人,三个Scrum团队并行开发,develop分支的同步成本远大于收益。我们让所有feature分支直接从master拉出,通过Gerrit的rebase策略保证合入时master始终领先。
  2. 强制rebase而非merge:Gerrit配置了--rebase策略,合入时自动变基,保证master历史上是绝对线性的。配合git log --first-parent查看主线条目一目了然。
  3. 提交信息规范:通过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,预计部署频率还能再翻一番。