一、问题背景:从“Git乱流”到“发布恐慌”

2023年Q2,我们团队经历了痛苦的扩张期。前端、后端、算法三个小组共40人挤在同一个仓库里,主干分支(当时叫master)上的提交记录像一团乱麻。每天下午四点半的集成窗口,成了全员的噩梦——平均每次合并要解决12个冲突文件,且经常出现“我本地跑得好好的,合并完就崩了”的灵异现象。

更棘手的是Code Review流程名存实亡。开发者为了赶进度,经常在Merge Request里塞入超过2000行代码,Reviewer根本看不过来。有一次,一个同事把.env.production的数据库密码硬编码提交进了master,直到上线前安全检查才发现。

我们意识到,问题不在工具,而在工作流缺乏约束和自动化。于是,我们决定用两个月时间,彻底重构基于Git的协作规范。

二、环境与版本:我们用什么“武器”

  • Git版本:2.39.1(重点使用git switchgit restore命令)
  • GitLab:15.11.3-CE(社区版,启用“合并队列”功能)
  • CI/CD:Jenkins 2.414.2 + 声明式流水线
  • 代码规范:ESLint 8.56.0 + Prettier 3.1.0 + Husky 8.0.3
  • 核心诉求:支持每天200+次合并,且保证main分支永远可部署。

三、方案设计:Trunk-based + 短生命周期分支

我们抛弃了复杂的Git Flow(它太适合远古时期的发布节奏了),转而采用Trunk-based Development的变体:

  1. 唯一长期分支main(禁止直接push,只允许Merge Request合并)。
  2. 短生命周期特性分支:命名规则为feature/ISSUE_ID-简短描述(如feature/2331-add-rate-limit)。
  3. 强制“小步快跑”:每个分支的改动量必须小于400行(通过CI检查),生命周期不超过3个工作日。
  4. 合并队列:GitLab的Merge Queue功能,让符合条件的分支按顺序自动合并,避免“合并瞬间”的冲突。

分支策略示意图(简略):

main (保护分支)
  ↑ merge (仅通过MR)
  |
  └── feature/2331-add-rate-limit (存活  3) {
                        error "分支 ${BRANCH_NAME} 存活时间 (${ageInDays.round(1)}天) 超过3天,请先rebase main分支"
                    }

                    // 3. 检查改动行数(对比origin/main)
                    def diffStat = sh(
                        script: "git diff --shortstat origin/main...origin/${BRANCH_NAME}",
                        returnStdout: true
                    ).trim()
                    echo "改动统计: ${diffStat}"
                }
            }
        }
        stage('Test') {
            steps {
                sh 'npm run ci:test' // 包含单元测试 + 覆盖率阈值检查
            }
        }
        stage('Build') {
            steps {
                sh 'npm run build:production'
            }
        }
    }
    post {
        failure {
            // 通知GitLab MR状态为failed
            updateGitlabCommitStatus name: 'jenkins-ci-check', state: 'failed'
        }
    }
}

踩坑提示git diff --shortstat 在Jenkins的detached HEAD模式下,需要确保已git fetch origin。我们在流水线第一步就加了sh 'git fetch --all --prune',否则拿不到最新分支。

五、踩坑与优化:那些文档里没告诉我的事

坑1:git merge vs git rebase 的团队认知战
一开始我们强制要求rebase来保持线性历史,但很多人rebase完就push -f,直接把别人的提交冲掉了。后来我们改用GitLab的Squash commits选项,在MR合并时强制squash,既保持了main历史干净,又不用开发者手动rebase。效果:git log --graph 变成了一条直线,排查问题用git bisect效率极高。

坑2:Merge Queue与CI的“死锁”
GitLab的Merge Queue在15.11有bug——如果CI跑了超过30分钟,队列会超时。我们把Jenkins的Test阶段拆成了并行任务(前端单元测试、后端接口测试),总耗时压到了12分钟以内,队列再也没堵过。

坑3:Code Review的“橡皮图章”现象
即使有模板,Reviewer还是会偷懒。后来我们引入了一个“反直觉”规则:如果MR少于100行,Reviewer必须标注“LGTM based on code reading”;如果超过300行,必须由两位Reviewer会签。这让大MR的通过率从90%降到了40%,逼迫开发者拆分提交。

六、效果数据:半年后的真实数字

  • 每日合并次数:从日均80次提升到210次(团队规模不变)。
  • 合并冲突率:从单次合并平均12个冲突文件,降到平均1.5个,且大部分是配置文件(package-lock.json)。
  • 分支平均存活时间:从5.2天→1.8天
  • CI失败率:从34%降低到8%(因为本地已经跑过lint-staged和单测)。
  • 线上紧急回滚:从每月3次降至0.3次(几乎不再出现“合并后立崩”)。

七、总结:流程是给“未来的自己”写的代码

Git工作流没有银弹。我们这套方案适合中等规模(20-50人)、发布节奏快(每周至少5次发布)的SaaS团队。如果你是做嵌入式或ToB私有化部署,可能需要更保守的Release Branch策略。

最后一点心得:任何流程都必须能对应到一个硬性检查。比如“不许提交大文件”这条,嘴上说没用,我们直接在pre-commit里加了git diff --cached --numstat | awk '{ if ($1 > 500) exit 1 }'。只有机器验证过的流程,才是真的流程。

如果你们团队也正在被Git冲突困扰,不妨先从小处着手:main分支保护起来,强制MR,然后加一个模板。跑通后再谈自动化,别一口吃成胖子。