4. 版本控制与分支策略

版本控制本身不复杂,真正让人头疼的是分支策略。你看,没有分支策略的仓库,就像一个没有交通规则的城市——到处是合并冲突、构建失败、误部署。我在不同团队待过好几次,最惨的一次是十几个人的项目用了七八种分支命名习惯,光找最终发布的版本就花了半天。所以说,分支策略本质上是一套协作契约,每个人遵守它,流水线才能稳定运转。

常见分支模型流程(Git Flow 示意) main / master develop feature/xxx feature/yyy release/x.y hotfix/xxx tags / v1.0.0 说明:实线箭头代表合并方向,特性分支合并到 develop,develop 合并到 release,release 和 hotfix 合并回 main 并打 tag

4.1 为什么流水线离不开分支策略

说白了,CI/CD 流水线就是围绕分支设计的。每次 push 到特定分支,触发不同阶段的自动化任务。举个例子:

  • 推送特性分支 → 只运行单元测试 + 代码扫描,快速反馈。
  • 推送到 develop → 触发完整的集成测试 + 构建制品。
  • 推送到 main → 走发布流水线,包括冒烟测试、签名、部署到预发环境。

我曾经见过一个团队把几十个功能挤在一条分支上,每次推代码都要跑两个小时的完整流水线,开发等结果等到崩溃。后来我们切了短特性分支加 trunk-based 策略,反馈时间降到了 10 分钟以内。

4.2 主流分支模型对比

模型 核心分支 适合场景 流水线复杂度
Git Flow main, develop, feature, release, hotfix 版本发布周期固定(按月/季度) 高(需要多条流水线规则)
GitHub Flow main, feature(通过 PR 合入) 持续部署,快速迭代 低(一条 main 分支 + 特性分支)
Trunk-Based main(trunk),极短期特性分支 每日多次合并,对测试信心要求高 低(主要保护 trunk)

我个人的习惯:如果团队在 10 人以内、没有严格的发版窗口,直接用 Trunk-Based 最好。管理成本低,流水线只有一条主线规则。但如果产品交付给外部客户、需要明确版本号,Git Flow 更稳妥,只是你得把 release 和 hotfix 的流水线规则写清楚。

4.3 分支命名规范——细节决定成败

分支名字看着是小事,但会影响自动化规则的匹配。我建议用前缀 + 短标识的方式,且全部小写,用斜杠分割。比如:

# 特性分支
feature/jira-1234-user-login
# 缺陷修复
bugfix/jira-5678-null-pointer
# 热修复
hotfix/v1.2.1-security-fix
# 发布分支
release/v2.0.0
# 实验性分支
experiment/perf-tune-query

为什么要统一?因为你可以用 CI 的正则规则自动识别:

# 示例 .gitlab-ci.yml 中的规则
only:
  - /^feature\/.*$/
  - /^bugfix\/.*$/
except:
  - /^release\//

我有一个深刻的教训:之前团队有人用大写字母,有人用下划线,结果 CI 的 branch filter 写了一大堆 or 条件,维护起来痛苦得要命。后来我们推行了强制命名规范,并在 pre-commit hook 里校验,从此这类问题绝迹。

4.4 分支保护规则——谁也不能直接往主分支推

保护分支是流水线安全的基石。没有保护规则,任何人都能绕过 CI 直接合并,那流水线自动化就形同虚设。建议至少设置以下几条:

# 以 GitHub 保护规则为例
Branch name pattern: main
☑ Require pull request before merging
☑ Require status checks (至少包含 CI pipeline 成功)
☑ Require branches to be up-to-date
☑ Include administrators
☑ Require linear history(可选,但推荐)

我曾经遇到过:某个紧急热修复,管理者绕过 PR 直接 push 到 main,结果漏掉了代码扫描,一个低危安全问题就这么流到了生产环境。后来我变成规则强制:即使管理员也必须走 PR + CI。紧急情况可以临时窗口,但必须事后复盘。

4.5 合并策略的选择:merge vs squash vs rebase

策略 提交历史 适用场景
merge commit 保留所有分支历史,有 merge 节点 团队习惯看到完整分叉,评审时容易追踪
squash merge 将特性分支所有 commit 压缩成一个 特性分支包含大量“wip”、“fix typo”等琐碎提交
rebase + fast-forward 线性历史,没有 merge 节点 追求干净、可读的历史,适合 trunk-based

我个人倾向在 trunk-based 模型里用 rebase + fast-forward,因为历史像线一样直,回溯问题非常方便。但要注意:rebase 会改写提交,不要在多人共享的分支上乱 rebase。

4.6 分支策略与流水线的联动配置

通过几个典型场景来感受一下具体怎么写。下面是一个 GitLab CI 片段,根据分支类型触发不同 job:

stages:
  - lint
  - test
  - build
  - deploy

variables:
  DOCKER_TAG: $CI_COMMIT_BRANCH

lint-job:
  stage: lint
  script: npm run lint
  only:
    - /^feature\/.*$/
    - /^bugfix\/.*$/
    - develop
    - main
    - /^release\//*

test-job:
  stage: test
  script: npm run test:ci
  only:
    - develop
    - main
    - /^release\//*
    - /^hotfix\//*

build-job:
  stage: build
  script: docker build -t myapp:$DOCKER_TAG .
  only:
    - develop
    - main
    - /^release\//*
    - /^hotfix\//*

deploy-staging:
  stage: deploy
  script: deploy staging
  only:
    - develop
  environment: staging

deploy-production:
  stage: deploy
  script: deploy production
  only:
    - main
  when: manual
  environment: production

看到没?特性分支只跑 lint,降低反馈成本;develop 跑完整的测试和构建,部署到 staging;main 发布生产,且需要手动确认。这样每一个分支都获得恰到好处的自动化验证。

4.7 常见坑与避坑指南

  • 分支数量失控:长期不清理的死分支仍然触发 CI,浪费资源。我建议设置策略:合并后自动删除远程分支,定期扫描超过 30 天的旧分支并提醒。
  • 保护规则遗漏管理员:管理员直接 push 导致 CI 被跳过。请设置为“包括管理员”也强制检查。
  • 大特性分支长时间不合并:冲突越来越多,CI 通过率越来越低。我建议把特性分支拆成多个小批次,每 1-2 天往主分支方向合并一次。
  • 热修复分支直接合到 main 却忘掉 develop:会出现下次发布时修复内容丢失。记得在 hotfix 合并到 main 后,立即 cherry-pick 到 develop。

一句话总结:分支策略不是束缚,而是让流水线自动运转的轨道。选一个适合你们团队的模型,配上命名规范、保护规则和匹配的 CI 规则,剩下的事情交给机器就好。