合并回流与定制隔离
研发开发规范
- 理解为什么 custom 分支绝对不能合进 release
- 知道 hotfix 修完要回流到哪两个地方、为什么还不许改库
- 明白为什么没现场验证就不许回流,以及定制/产品线怎么从通用版取更新
- 合并 merge
- 把一个分支的改动融合到另一个分支,通常是从子分支合到主线。
- cherry-pick
- 从一个分支「摘」某几个 commit 到另一个分支,不合并整个分支。就像从别人的果园里只摘几个苹果。
- 回流
- 把修复/功能从子分支合并回主线(release),保证主线包含所有修复。我们要求 hotfix 必须「双向回流」:同时回到原版本和新版本。
- MR Merge Request
- 在 GitLab 上发起的合并请求,需他人审查通过后才能合并。就像代码的「审批流程」。
这是本规范最重要的一节,直接针对「定制误合并到新版本」的问题。
custom/ 永不合入 release- 客户定制分支(
custom/<版本>/客户-功能)仅用于交付该客户现场,交付方式为「从该custom/分支单独打安装包」。 - 严禁
git merge custom/x.x.x/xxx → release/x.x.x,严禁git cherry-pick把定制代码搬进通用版本。 - 定制内容需在
doc/定制/备案,无文档不分定制:详细的文档组织、模板与定制 SQL 隔离规则见 §2.9。
为什么要这么严格?因为 release 是所有客户共享的通用版本。定制代码一旦混进 release,下次任何客户升级都会带上它,可能出现「A 客户的功能在 B 客户现场触发了莫名其妙的 bug」,而且极难排查。把定制关在 custom 分支里,是从源头避免这种污染。
feature/ 与 hotfix/ 分支必须经现场(或与现场等价的实际环境)部署验证通过后,方可回流合入 release。
- 门禁目的:保证 release 分支头始终对应「现场已验证状态」。这也是 01·分支生命周期「feature/hotfix 从 release 分支头拉取」的前提——基线可信,「验证产物 = 回流产物」才成立,两条规则互锁。
- 验证记录:验证通过后按模板复制到
doc/branch/<分支路径>/验证记录.md填写(环境、日期、验证人、范围、结果,模板见 09·现场验证记录);回流合并前核对记录存在且结论为「通过」,无记录不得合并。 - 未验证分支的处置:只允许停留在本分支或部署到验证环境,禁止
git merge/cherry-pick进任何 release(含开发中版本)。 - 开发中版本尚无现场时,feature 以「与现场等价的实际环境」(联调环境 / 试点现场)验证代替,验证记录须注明环境类型。
为什么要有这道门禁?发布版本化部署的产品,「现场跑得过」是唯一可信的质量证据。没有门禁时,release 头会混入未验证代码,此后所有从 release 头拉出的分支、所有下行同步到客户/子产品的内容都被污染——门禁把「验证」从口口相传变成可核对的记录。
hotfix/ 必须双向回流,且禁止改库现场修复分支必须回流到两个地方:
- 原版本 release(如
hotfix/2.1.0/xxx→release/2.1.0),保证该版本后续现场拿到修复。 - 下一个开发中的 release(如 →
release/2.1.1),保证修复进入新版本。
hotfix 分支禁止任何数据库变更:不建表、不改列、不插数据。hotfix 面向已发布版本的现场快速修复,DB 变更意味着现场需执行升级脚本,风险与节奏都不匹配。已发布版本确需改库的项目 → 走 custom/ 定制分支交付(SQL 隔离见下方定制 SQL 隔离);通用性的 DB 修复 → 进下个开发中版本,或多现场紧急时走维护性微版本(规则五)。
# 现场修复完成后 git checkout release/2.1.0 && git merge hotfix/2.1.0/xxx git checkout release/2.1.1 && git merge hotfix/2.1.0/xxx # 或 cherry-pick # 然后删除 hotfix 分支 git branch -d hotfix/2.1.0/xxx
为什么必须回流两个地方?只回 release/2.1.0,老版本现场修好了,但新版本(2.1.1)里这个 bug 还在,下批客户升级后又踩坑;只回 release/2.1.1,老版本现场继续带病运行。双向回流确保「修过的 bug 永远不再出现」。
custom/<版本>与line/<产品>/<版本>从对应 release 分支头拉取(见 01·分支生命周期),并定期 merge 该 release 保持下行同步——release 头即「tag + 历次已验证修复」,同步一次即把全部通用修复带给客户/子产品。- 原「hotfix → custom 逐个 cherry-pick」方式由整体同步取代(hotfix 回流 release 后随同步带入);仅在客户明确只要个别修复时,才用 cherry-pick 精确控制,方向仍只能 hotfix → custom。
- 方向严格单向:release → custom/line;禁止 custom/line → release(防定制/子产品代码泄漏到通用版)。
# [✓] 正确:下行同步,把已验证修复带给定制/产品线 git checkout custom/2.1.0/huzhou-xxx git merge release/2.1.0 # [✓] 客户明确只要个别修复时:精确 cherry-pick git cherry-pick <hotfix-commit-hash> # [✗] 禁止:把定制搬进通用 git checkout release/2.1.1 git merge custom/2.1.0/huzhou-xxx # 绝对不允许
为什么方向只能是 release → custom/line?想象水流方向:通用修复是「干净的自来水」,可以接进任何杯子(定制/子产品);但定制代码是「加了料的饮料」,倒回自来水厂会污染整个供水。反向合并是定制污染通用版最隐蔽的途径,GitLab 服务端也会拦截这种合并。
为什么不再逐个 cherry-pick 通用修复给定制?逐个搬有两个必然的后果:漏搬——hotfix 一多,没人记得清哪个客户搬过哪个;漂移——各客户拿到的修复集合不一致,同一个 bug 在 A 客户修过了、B 客户现场还在复发。整体同步一次 merge 就把「tag + 全部已验证修复」带给客户,所有客户的修复集合永远一致。
hotfix 禁改库(规则三)+ 已发布版本禁 feature(01·命名细则)之后,「多个现场都需要的通用数据库修复」缺少轻量出口,逐现场走 custom 成本过高。为此设维护性微版本(四段版本号):
- 触发条件(满足其一):① 影响多个现场的通用数据/结构缺陷;② 一批已验证 hotfix 累积到值得发补丁包。单现场问题仍走 custom,纯代码 bug 仍走 hotfix。
- 流程:从对应
release/<X.Y.Z>拉出release/<X.Y.Z>.<N>(N 从 1 起,如release/2.1.0.1)→ 集中完成修复(允许改库,SQL 按主题序号汇总进sql/release/<X.Y.Z>.<N>/,规则同 06·SQL 版本化)→ 现场验证(规则二门禁同样适用)→ 打 tagvX.Y.Z.N→sql/tag/<X.Y.Z>.<N>/冻结 → 各现场按序执行增量。子产品同理,tag 如eos-v1.0.0.1。tag 段数即类型:三段 = 正式版,四段 = 微版本。 - 回流:与 hotfix 相同的双向回流——合回原
release/<X.Y.Z>与下个开发中 release;代码侧整体 merge,SQL 侧按主题并入下个开发中版本对应的序号文件(原版本线保留微版本目录作为补丁包交付物)。 - 频率约束:微版本是例外通道不是常态。同一版本线微版本超过 3 个仍未升级大版本的,应推动现场升级而非继续打补丁。
为什么微版本超过 3 个就该推动现场升级?补丁链越长,现场的版本组合越碎:同一版本线上同时跑着 v2.1.0、v2.1.0.1、v2.1.0.2……每个现场要执行的 SQL 序列各不相同,排查问题时得先对齐「你到底在哪个补丁上」。微版本是应急通道,升级大版本才能把所有现场收敛回同一条版本线。
提交前自问:这段代码/SQL 是否所有现场都适用?
- 是 →
feature/或hotfix/,可回流 release。 - 否(仅某客户需要)→
custom/,禁止回流。 - 否(属某子产品专属,非单客户)→ 归入对应产品线
line/<产品>/,回流规则见 01·产品线回流。
放通用分支(feature/hotfix)
- 修复所有现场都会遇到的 bug
- 新增所有客户都用得到的功能
- 优化通用性能、改通用 SQL 结构
放定制分支(custom)
- 只对接某客户特有的第三方系统
- 某客户要求的特殊报表/大屏布局
- 针对某客户硬件的特殊适配
本地 Hook 可被绕过,服务端保护才是最后防线。本项目托管于公司私有 GitLab,由仓库 Owner/Maintainer 在 Settings → Repository 配置:
| 分支模式 | 允许合并 | 允许推送 | 说明 |
|---|---|---|---|
main | Maintainer | No one(仅 MR) | 生产主线,禁直推 |
release/* | Maintainer + Developer | No one(仅 MR) | 版本主线,禁直推,强制 Code Review |
line/* | Maintainer + Developer | No one(仅 MR) | 产品线主线,禁直推;下行同步允许、上行默认禁止(详见 01·回流规则) |
效果:
main/release/*/line/*禁止git push直推,所有改动必须走 Merge Request。- MR 需至少 1 个 Maintainer 审批 + CI 通过方可合并,杜绝一人静默合入。
- 配合 §2.6,
custom/*→release/*的合并会在 MR 审查环节被发现并拒绝。
| Tag 模式 | 允许创建 | 说明 |
|---|---|---|
v* | Maintainer | 主产品版本 tag 仅 Maintainer 可打,避免误发版本 |
*-v* | Maintainer | 子产品 tag(如 eos-v2.1.0)仅 Maintainer 可打 |
在 Settings → Repository → Push Rules 配置:
- Commit message 正则:
^(feat|fix|docs|refactor|chore|perf|test|style|ci|build|revert)(\(.+\))?: .+|^(Merge|Revert) .*
服务端再次拦截违规提交信息(兜底本地被绕过的 Hook)。
- 禁止提交文件名:
hs_err_pid.*\.log|replay_pid.*\.log|\.DS_Store|log\.bin|\.iml$ - 最大文件大小:如 20MB,防止误传大文件(JVM heap dump 等)。
- 拒绝未引用 issue 的提交:按需开启(强制
Closes #xxx)。
MR 标题与描述沿用 §2.5 提交规范,描述模板建议包含:
## 变更类型 - [ ] feature 新功能 - [ ] hotfix 现场修复 - [ ] custom 客户定制(标注客户名) ## 关联版本 release/x.x.x / tag vX.Y.Z ## 变更说明 (做了什么、为什么、影响范围) ## 测试 - [ ] 自测通过 - [ ] 影响数据库的已补 sql/ 下对应分支目录的脚本(见 06·分支目录映射)并登记 CHANGELOG
配合 §2.6:MR 描述里若勾选「客户定制」,Review 时必须确认目标分支不是 release/*。
定制分支(custom/<版本>/<客户>-<语义>)可长期存在,但若不记录「改了什么、基于哪个版本、交付哪个现场」,时间一久就无人能说清,现场出问题无从追溯。
强制要求:每个客户定制必须有对应文档,且与 custom/ 分支一一对应。
- 每个客户一个目录(
doc/定制/<客户>/),客户名用中文或约定代号(与 §2.2custom/分支名一致)。 - 每次定制一个
<功能>.md,对应一个custom/分支。 - 客户目录下一个
README.md汇总该客户所有定制,便于快速查阅。
doc/定制/README.md)记录所有定制客户一览,字段:客户、联系方式、维护人、定制数量、最近交付版本、客户目录链接。
doc/定制/<客户>/README.md)汇总该客户所有定制,字段:定制项、对应分支、基线版本、交付现场、状态、维护人、文档链接。
doc/定制/<客户>/<功能>.md)每次定制必须包含以下章节:
| 章节 | 内容 |
|---|---|
| 基本信息 | 客户、交付现场、需求来源(合同/口头/工单)、提出人、日期 |
| 基线与分支 | 基于哪个版本的 release/tag、对应的 custom/ 分支名、维护人 |
| 需求描述 | 定制了什么功能、与通用版的差异、为什么不能进通用版 |
| 改动清单 | 涉及的代码文件、模块、接口(列关键文件路径,非全量) |
| 数据库变更 | 是否改库表/数据,定制 SQL 放哪、如何与通用 SQL 隔离 |
| 交付与部署 | 安装包版本号、交付日期、部署现场、回滚方式 |
| 与通用版的同步策略 | 是否跟随通用 hotfix cherry-pick、升级到新版本时的处理 |
| 风险与注意事项 | 已知问题、特殊配置、不能改动的地方 |
定制涉及数据库变更时,SQL 不得混入通用 sql/ 目录(含 sql/release/、sql/tag/ 等,见 06·SQL 版本化),独立存放:
理由:通用 sql/ 按版本顺序执行到所有现场,定制 SQL 只能执行到对应客户,二者混放会导致通用现场误执行定制脚本。
- 通用修复需应用到客户时,按规则四对对应 release 做下行同步(merge),并在定制文档「与通用版的同步策略」章节登记;客户明确只要个别修复时,才用 cherry-pick 精确控制。
- 客户升级到新版本时,需重新评估定制项:定制功能是否已纳入通用版(若是则废弃定制分支)、是否需基于新版本 release 重建
custom/分支。 - 定制分支禁止向任何
release/*合并(规则一),违反即定制代码污染通用版。
我 hotfix 只合并回了 release/2.1.0,忘了回 release/2.1.1,会怎样?
我觉得定制里那段代码挺好,想加到通用版本里给大家用,可以吗?
feature/ 或 hotfix/ 分支上重新实现这段功能(基于通用版的角度),让它成为通用代码的一部分,再正常回流 release。直接 cherry-pick 定制代码会把定制特有的依赖、配置一起带过来。我不小心把 custom 合进了 release,已经 push 了,怎么办?
git revert 撤销那次合并提交(不要 force push 改历史,除非还没人拉取)。然后排查是否已有客户升级到了受影响的版本。GitLab 的保护分支规则本应拦截这种操作——如果没有拦住,说明分支保护配置需要检查。hotfix 回流时用 merge 还是 cherry-pick?
git merge 最简单。但如果 hotfix 分支上混了其他不该回流的提交,用 git cherry-pick <commit-hash> 精确挑选那一个修复提交更安全。回完后记得 git branch -d 删掉 hotfix 分支,保持仓库整洁。什么叫「现场验证通过」?我在测试环境验过算不算?
doc/branch/<分支路径>/验证记录.md 且结论为「通过」,没有这份记录,回流合并就会被拒绝(规则二门禁)。