多人往同一个模块塞代码,合并顺序和优先级没定好,逻辑覆盖的 bug 修到怀疑人生

代码合并出问题,根子从来不在 Git 操作上。Git 只是个忠实的记录员,它只会告诉你“这里有两个版本,我不知道哪个对”,真正把逻辑写坏的是人——是合并前没人想清楚“谁的改动应该赢”,以及“赢了之后,输掉的那部分逻辑该怎么补”。

这就是所谓的合并优先级问题。我见过太多团队把合并当成拼积木,看到冲突就机械地选一边,或者干脆谁的 commit 新就留谁的。这种偷懒做法,后果就是线上出现那种让你半夜惊醒的 bug:上个月修好的支付回调校验,这个月又坏了,因为另一个人合并时直接把那段校验代码覆盖掉了。

要根治这个问题,得从三个层面入手:执行顺序、决策机制、验证闭环

执行顺序:谁先合,谁后合,不是一个技术问题

大多数团队对合并顺序的理解停留在“冲突少一点就好”,这完全搞错了重点。正确的策略是:风险越高的分支,越晚合并

具体来说,应该建立这样的优先级倒排规则:

  • P0 级分支优先合入。线上 hotfix,紧急安全补丁,这类分支永远是第一个进主干的。它们改动范围小、回滚代价高,必须最先落地,让后续所有分支都在修复后的代码基上开发。
  • 大规模重构分支最后合入。如果一个分支改了 30 个文件、涉及 6 个模块的接口变更,它必须是最后一个。不是因为怕冲突,而是因为它合入之后,其他所有还在并行的分支都需要基于新接口重新适配。先合重构,等于让所有人原地停下做适配,这在 sprint 中后期就是灾难。
  • 同模块改动,按业务优先级串行。假设 A 团队在改订单模块的退款流程,B 团队在改同一个模块的优惠券核销逻辑,两个改动都触及 OrderService.cancel() 方法。这时候不能各合各的,必须让业务优先级更高的一方先合,另一方 rebase 之后再合。谁的业务优先级高?不是谁先提的 PR,而是谁的需求对应着更大的线上事故风险或营收影响。

执行顺序定下来之后,下一个要解决的问题更棘手:当冲突发生,谁的代码活下来?

决策机制:用“逻辑归属人”替代“代码作者”

Git 在冲突时会告诉你“当前分支”和“合并分支”的差异,但从不告诉你这两个版本各自要解决什么问题。所以机械地选“保留我的”或“保留他的”都是错的。冲突解决不应该由合并者一人拍板,必须由改动的逻辑归属人确认

这里的“逻辑归属人”不是代码作者。代码作者是写这段代码的人,逻辑归属人是理解这段代码要解决什么业务问题、知道改了之后会影响哪些上下游的人。很多时候代码作者已经转岗或离职了,但逻辑归属人还在。

具体落地方案:

  1. 每个模块在仓库里维护一个 OWNERS 文件,不是 GitHub 那种形式化的 CODEOWNERS,而是明确标注:订单模块逻辑归属人张三,支付回调逻辑归属人李四。这个文件跟着代码走,版本可控。
  2. 任何涉及跨模块冲突的合并,合并者必须把冲突代码和两侧的 commit message 贴到指定频道,at 双方的逻辑归属人,两人都确认后才能合。这不是流程冗余——我见过一个案例,订单模块改了一个字段的默认值从 null 变成 0,支付模块的代码一直用 null 判断是否已退款,合并时没人注意到这个差异,上线后所有退款单都被判定为“未退款”,客户收到重复退款短信。这个 bug 的修复成本是合并时那 5 分钟确认时间的 100 倍。
  3. 对于同一段代码被多人反复改的情况,强制要求最后一个合并的人在 commit message 里写清楚:这段逻辑覆盖了之前哪个 commit 的哪部分改动,以及为什么。Git 本身不会帮你记录“覆盖意图”,commit message 是唯一的追溯手段。

规则定好了,但光靠人的纪律撑不住。必须有自动化的验证闭环。

验证闭环:让覆盖行为在 CI 阶段就暴露

逻辑覆盖的 bug 之所以可怕,是因为它往往不产生冲突标记。两个人改了同一个函数的不同行,Git 自动合并成功,但组合起来的逻辑是错的。这种“静默覆盖”靠人工 review 很容易漏,必须靠测试用例捕获。

核心原则是:每个分支合入主干前,必须跑“对方分支的测试用例 + 自己的测试用例”的完整集合

实操上分三步:

  1. 分支在提 PR 时,CI 不只跑本分支的测试,而是把当前主干的最新代码 merge 进来之后再跑全量测试。很多团队只跑分支自己的测试,这等于没跑——你的代码在自己分支上当然是对的,问题出在跟别人的代码组合之后。
  2. 建立模块级别的回归测试标签。比如订单模块所有测试打上 @order-regression 标签,支付模块打上 @payment-regression。任何分支改了订单模块,CI 自动触发支付模块的回归测试。这不是过度设计,一次订单字段变更导致支付异常的概率远高于你的直觉。
  3. 对“逻辑覆盖高发区”做 diff 级别的告警。用脚本扫描 PR 的 diff,如果发现某个方法被删除了超过 3 行、或者某个条件判断的关键字被改了(比如 if (status == null) 变成了 if (status == 0)),自动在 PR 里打上高风险标签,要求至少两个逻辑归属人 approve。这个脚本可以很简单,用 git diff 配合正则就能实现,但效果立竿见影。

真正的根因:分支策略本身就在制造风险

说句可能得罪人的话:如果你发现团队频繁出现合并覆盖 bug,很可能是分支策略本身就错了。

长生命周期分支(long-lived branch)是逻辑覆盖的最大温床。一个分支活了 3 周,期间主干往前走了 200 个 commit,最后合并时产生的信息差已经大到任何人工 review 都无法完全消解。这就是为什么 trunk-based development 和短分支策略在业界逐渐成为主流——不是因为它更先进,而是因为它把合并的复杂度从“一次性解决 200 个 commit 的冲突”降解为“每天解决 5 个 commit 的冲突”。

如果你的业务特性决定了必须有长分支(比如大版本的定制开发),那就必须接受一个事实:长分支不能只是“延迟合并”,而必须是“持续同步”。每两天从主干往长分支 rebase 一次,冲突当场解决,逻辑覆盖当场暴露。拖到最后一天才 rebase,等于把所有风险集中爆发,谁也救不了。

合并顺序和优先级规则,本质上是一套风险管理机制。它要回答的问题不是“怎么让合并更顺利”,而是“当两个人的改动不能共存时,谁来决策、依据什么决策、怎么验证决策是对的”。把这三个问题回答清楚了,那些让你怀疑人生的覆盖 bug 才会真正消失。


常见问题

我们团队小,没有专门的逻辑归属人怎么办?

小团队不需要形式化的 OWNERS 文件,但“逻辑归属”这个概念仍然有效。最务实的做法是:谁最近改过这段代码,谁就是事实上的归属人,合并者直接拉对方语音确认。小团队的优势是沟通成本低,把这个优势用到极致,比任何流程工具都管用。但记得在 commit message 里记录确认结论,三个月后你会感谢现在的自己。

CI 跑全量测试太慢了,每次 PR 等 40 分钟受不了怎么办?

不需要每次都跑全量。关键是把测试按模块拆分,CI 根据 diff 自动选择需要跑的测试集合。如果你的项目用了 Bazel 或类似支持依赖分析的工具,这一步可以做到零配置。如果是传统 Maven/Gradle 项目,花一个下午写个脚本,根据变更文件的路径匹配对应的测试模块,投入产出比极高。另外,慢不是跳过测试的理由,逻辑覆盖 bug 修一个小时的工时,够跑 90 次 40 分钟的 CI。

主干已经往前走了很多,rebase 冲突太多,团队成员抵触怎么办?

抵触 rebase 通常不是因为 rebase 本身难,而是因为一次性冲突太多。解决方法是提高同步频率——每天从主干往分支同步,而不是攒一周。如果冲突仍然多,说明多个团队在密集修改同一个模块,这时候不是技术问题了,是架构边界和任务分配的问题。把频繁冲突的模块拆小,或者让改同一个模块的人坐在一个群里实时沟通,比研究 rebase 技巧有用得多。