hotfix 合入 release 分支后,用什么流程保证补丁不漏合到 develop 分支也不被重复合并

补丁漏合到 develop 的根源不是「忘了」,而是流程里没有强制约束。要保证不漏合、不重复合并,核心是把「合入顺序」和「基线对齐」固化成硬性规则,而不是靠开发者的记忆。下面是一套在生产环境验证过的完整流程。

先明确分支模型与合入顺序

如果你用的是 Git Flow 变体,hotfix 分支从 release(或 master)拉出,修复完成后通常有两个目标:当前 release 分支(发布线上)和 develop(下一个开发版本)。关键规则是:先合 release,再合入 develop,且 develop 必须通过 merge 而非 cherry-pick 来接收 hotfix 内容。

为什么不用 cherry-pick?因为 cherry-pick 会生成新的 commit hash,两个分支上的同一修改在 Git 看来是两个完全不同的提交。后续 develop 合回 release 或做分支比对时,Git 无法自动识别它们之间的等价关系,轻则产生冲突,重则出现重复合入。merge 保留原始 commit hash,Git 的 merge base 机制可以自动处理「这个提交已经在目标分支上」的情况。

强制 merge 顺序:先 release 后 develop

具体操作如下:

# 1. 在 release 分支上完成 hotfix 合入
git checkout release/1.8
git merge --no-ff hotfix/order-crash-fix
git push origin release/1.8

# 2. 回到 develop 合入同一个 hotfix 分支
git checkout develop
git merge --no-ff hotfix/order-crash-fix
git push origin develop

--no-ff 保留 hotfix 分支的合并节点,让历史清晰可追溯。更重要的是,两次 merge 使用的是同一个 hotfix 分支的同一个 commit,Git 会在 develop 上记录这些 commit 的原始 hash。将来 develop 合并回 release 时,Git 发现这些 commit 已经存在于 release 侧(通过 merge base 比对),会自动跳过,不会产生重复。

如果顺序反了——先合 develop 再合 release——不会造成技术灾难,但会增加风险:release 侧的 hotfix commit 可能被后续 develop 的 merge 覆盖或引入冲突。先合 release 还有一个现实原因:线上问题优先修复发布,release 分支是发布源,先合它意味着可以立刻打 tag 发版。

用 merge 而非 rebase 处理 develop 的同步

很多人习惯在 develop 上做 rebase 保持线性历史,但 hotfix 场景下 rebase 会重写 commit hash,破坏「同一提交在两个分支上可被 Git 识别」的前提。如果团队在 develop 上强制 rebase,hotfix 合入 develop 时就不能用 rebase,必须用 merge。 这个规则要写进团队协作文档,并且在 CI 里做校验。

实际操作中,如果 develop 上有大量并行开发,merge hotfix 时可能产生冲突。解决冲突时注意:只解决冲突本身,不要顺手把其他无关改动带进来。冲突解决后生成的 merge commit 仍然保留了 hotfix 的原始 commit 作为 parent,不影响后续去重。

用 CI 自动化校验合入状态

光靠规则不够,必须有自动化检查。在 CI 流水线里加一个「hotfix 合入检查」步骤,核心逻辑如下:

# 检查 develop 是否包含 release 上最近 N 天内的所有 hotfix commit
# 假设 hotfix 分支命名规范为 hotfix/*
RECENT_HOTFIX_COMMITS=$(git log release/1.8 --since="7 days ago" \
  --grep="Merge branch 'hotfix/" --format="%H")

for commit in $RECENT_HOTFIX_COMMITS; do
  if ! git merge-base --is-ancestor $commit develop; then
    echo "ERROR: hotfix commit $commit not merged to develop"
    exit 1
  fi
done

这段脚本会在每次 develop 分支有 push 时运行,检查 release 上最近 7 天的 hotfix merge 是否已经全部存在于 develop。如果有遗漏,CI 直接失败,阻止后续流程。--is-ancestor 是 Git 原生命令,判断一个 commit 是否是另一个 commit 的祖先,精确且无副作用。

实际落地时,可以把时间窗口从 7 天放宽到 14 天或 30 天,取决于团队的发布节奏。如果 release 分支已经归档,就把检查目标改为「当前活跃的 release 分支」。

处理多个并行 release 分支的情况

如果团队同时维护多个 release(比如 1.7 还在支持,1.8 刚发布,1.9 在开发),hotfix 可能需要合入多个 release。规则是:hotfix 从最老的受影响 release 拉出,修复后依次合入所有受影响的 release 和 develop,顺序从老到新。

# 假设 hotfix 影响 1.7 和 1.8
git checkout release/1.7
git merge --no-ff hotfix/security-patch
git checkout release/1.8
git merge --no-ff hotfix/security-patch
git checkout develop
git merge --no-ff hotfix/security-patch

从老到新的顺序保证了每个版本的 merge base 都是正确的。如果从新到老,老版本上可能缺少新版本已经引入的某些前置提交,导致冲突或语义错误。

多个 release 的合入检查也要相应扩展:CI 脚本遍历所有活跃 release 分支,检查它们各自的 hotfix commit 是否都存在于 develop。

防止重复合并的兜底机制

即使所有规则都遵守,偶尔也会出现人为失误——比如有人手动 cherry-pick 了一次,或者重复 merge 了同一个 hotfix 分支。Git 的 merge 机制本身能防止大部分重复:如果 hotfix 分支的所有 commit 已经存在于目标分支,merge 会直接输出「Already up to date」并跳过。

但 cherry-pick 造成的重复无法被 merge 自动识别。针对这种情况,可以在 CI 里加一个「重复提交检测」:比较 release 和 develop 上最近 hotfix 相关的 patch-id。

# 提取 release 上 hotfix 提交的 patch-id
RELEASE_PATCH_IDS=$(git log release/1.8 --since="30 days ago" \
  --grep="hotfix" --format="%H" | xargs git patch-id | awk '{print $1}')

# 检查 develop 上是否有相同 patch-id 但不同 hash 的提交
for patch_id in $RELEASE_PATCH_IDS; do
  DEVELOP_MATCHES=$(git log develop --format="%H" | \
    xargs git patch-id | grep "^$patch_id" | wc -l)
  if [ $DEVELOP_MATCHES -gt 1 ]; then
    echo "WARNING: duplicate patch detected: $patch_id"
  fi
done

git patch-id 基于 patch 内容生成哈希,cherry-pick 产生的提交虽然 hash 不同,但 patch-id 相同,可以被检测出来。这个检查作为 warning 而非 error,提醒人工介入确认。

常见问题

为什么不能直接用 cherry-pick 把 hotfix 合到 develop?

cherry-pick 会生成新的 commit hash,导致同一修改在两个分支上被 Git 视为不同提交。后续 develop 合回 release 时,Git 无法识别这些提交已经存在,会产生冲突或重复合入。更麻烦的是,如果 hotfix 包含多个 commit,逐个 cherry-pick 容易漏掉中间某个提交,而且历史记录上完全看不出这些提交之间的关联。

hotfix 合入 develop 时产生冲突怎么办?

正常解决冲突即可,但只解决冲突本身,不要引入无关改动。冲突解决后生成的 merge commit 仍然保留 hotfix 的原始 commit 作为 parent,不影响后续去重。如果冲突很复杂,可以考虑先从 release 合入一次 develop(把 release 的最新状态同步过来),再合 hotfix,这样冲突范围会缩小。

如果已经用了 cherry-pick,现在怎么补救?

已经 cherry-pick 的提交无法自动「转换」成 merge,但可以手动修复:在 develop 上找到 cherry-pick 的提交,用 git revert 撤销它,然后重新用 git merge --no-ff hotfix/xxx 合入。revert 会生成一个反向提交,抵消 cherry-pick 的效果,之后 merge 可以正常工作。注意 revert 本身也要合回 release,否则两边又会不一致。

CI 检查应该放在哪个阶段?

放在 develop 分支的 push 阶段最合适,每次有人推代码到 develop 就触发检查。如果检查失败,直接 block 合并请求。不建议放在定时任务里,因为定时任务有延迟,可能已经有人基于缺了 hotfix 的 develop 开始开发了。