一、问题背景:当仓库变成“分支动物园”

去年Q3,我们团队从5人扩展到20人,Git仓库开始失控。具体症状:

  • 长期存在30+个feature分支,最老的活了4个月
  • main分支平均每天收到12个合并请求,其中3个会引发冲突
  • 上线流程靠“老王记得要cherry-pick哪几个commit”
  • 每次release前,至少2人花一整天做集成

根本原因不是工具,而是没有明确的分支策略和自动化门禁。我们决定系统性重构。

二、环境与版本

  • Git:2.40.1(启用merge.conflictStyle=zdiff3
  • 代码托管:GitLab 16.8.2(自建,Runner 16.8)
  • CI/CD:GitLab CI + GitHub Actions(双轨,部分项目迁移中)
  • 关键插件:git-lfs 3.4.0pre-commit 3.5.0
  • 语言栈:Java 17 + Maven 3.9.6、Node 20.11 + pnpm 8.15

三、方案设计:主干开发 + 短生命周期分支

我们对比了三种模型:

  1. Git Flow:太重,develop分支与main长期偏离,不适合每周多次发布
  2. GitHub Flow:简单,但缺少预发环境对应分支
  3. Trunk-Based Development (TBD):主干始终可发布,分支存活 commitlint.config.mjs
    • npx commitlint --from $CI_MERGE_REQUEST_DIFF_BASE_SHA --to $CI_COMMIT_SHA
      rules:
    • if: $CI_MERGE_REQUEST_ID

build-java:
stage: build
image: maven:3.9.6-eclipse-temurin-17
script:
- mvn -B -DskipTests clean package
artifacts:
paths:
- target/*.jar
expire_in: 1 day

unit-test:
stage: test
image: maven:3.9.6-eclipse-temurin-17
script:
- mvn -B test
coverage: '/Total.?([0-9]{1,3})%/'
artifacts:
reports:
junit:
- target/surefire-reports/TEST-
.xml
rules:
- if: $CI_MERGE_REQUEST_ID
- if: $CI_COMMIT_BRANCH == "main"

sast-scan:
stage: security
image: returntocorp/semgrep:1.52.0
script:
- semgrep ci --config=auto --sarif --output=semgrep.sarif
artifacts:
reports:
sast: semgrep.sarif
allow_failure: false
rules:
- if: $CI_MERGE_REQUEST_ID

deploy-preview:
stage: deploy-preview
image: bitnami/kubectl:1.29
script:
- kubectl set image deployment/app app=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA -n preview-$CI_MERGE_REQUEST_IID
- kubectl rollout status deployment/app -n preview-$CI_MERGE_REQUEST_IID --timeout=120s
environment:
name: preview/$CI_MERGE_REQUEST_IID
url: https://mr-$CI_MERGE_REQUEST_IID.preview.example.com
on_stop: stop-preview
rules:
- if: $CI_MERGE_REQUEST_ID

### 4.3 Code Review流程与自动化检查

我们使用GitLab的`CODEOWNERS` + MR模板:

```markdown

## 变更类型
- [ ] feat / fix / refactor / perf

## 关联JIRA
JIRA-___

## 测试说明
- 单测覆盖率变化:___%
- 手动验证步骤:

## 检查清单
- [ ] 已运行 `mvn verify`
- [ ] 已更新API文档(如适用)
- [ ] 无新增TODO/FIXME
- [ ] 数据库变更已包含迁移脚本

CODEOWNERS文件:

# .gitlab/CODEOWNERS
* @backend-leads
/src/main/java/com/example/payment/ @payment-team @security-team
/infra/ @devops-team
*.sql @dba-team

Review规则:
- 至少1个owner approval
- 作者不能self-approve
- 新文件>500行或删除>200行需2个approval
- 所有CI job必须pass,且semgrep无high/critical

五、踩坑与优化

坑1:Fast-forward合并导致历史丢失
最初用--ff-only,结果MR的commit信息全丢。改为--no-ff并保留merge commit,方便追溯。

坑2:CI缓存key导致依赖串包
$CI_COMMIT_REF_SLUG作为cache key,feature分支之间不共享,但main与feature不共享导致每次全量下载。改为key: "$CI_COMMIT_REF_PROTECTED-$CI_JOB_NAME",保护分支共享缓存。

坑3:pre-commit钩子被绕过
有人用--no-verify提交。我们在CI的commit-lint阶段做二次校验,并设置GitLab push rule拒绝不合规消息。

坑4:release分支cherry-pick冲突
每周四切release后,hotfix经常冲突。优化:hotfix必须先合并到main,再cherry-pick到release,且用-x记录来源。

性能数据
- 合并请求平均处理时间:4h → 25min
- CI流水线时长:从18min降到6min(缓存+并行)
- 生产回滚率:每月3次 → 0.9次
- 分支平均存活:11天 → 1.8天

六、总结

Trunk-Based不是银弹,但配合短生命周期分支、强制CI门禁和自动化Code Review,能把团队从“合并地狱”里拉出来。关键三点:

  1. 分支策略要匹配发布节奏:每周多次发布就别用Git Flow
  2. 自动化能解决的绝不靠人:commit lint、SAST、预览环境
  3. 规则要写进配置,不是WikiCODEOWNERSpush-rules、CI job才是真门禁

下一步我们计划把release/*也纳入自动化cherry-pick,并尝试用merge queue进一步降低main的冲突率。