我所理解的 Git 最佳实践(5):共享测试分支与按需求部署
需求准备合入 main 前,QA 通常要先在测试环境验收一版能运行的代码。团队只有一套环境时,可以把待测需求合进共享 test,由 CI 部署这个分支。基础设施能按需求分配环境时,则可以为每个开发分支或 PR 单独部署一份。
假设 A、B 两个需求都从 main 开发。共享环境里,A 先提测,修完验收发现的问题后要单独上线。B 还在开发,等它跟上最新 main 再提测时,Git 却提示 test 无法快进。按需求部署时,A 和 B 可以各自更新自己的验收环境。
A 提测后继续追加修复
A 的第一个提交 A1 合入 test。本例用保留合并提交的方式,例如 git merge --no-ff,得到 T1。QA 验收时发现一个问题,开发者在原 feat/a 上追加修复 A2,用同样的方式合进 test,得到 T2。QA 在 T2 对应的部署上验证这轮修复。
这里保留 A1,后续 merge 就能认出它已经是 test 的祖先。要是提测后用 amend 把 A1 改成一个新提交,test 仍包含旧的 A1。再次合并时,Git 可能要从更早的共同祖先比较两份修改。两边碰巧修改同一处时,还可能冲突。追加 A2 既保留了之前部署过的版本,也让后续合并能沿着原来的提交链继续。
验收后,由 feat/a 向 main 发 PR,只合入 A。本例在合入时 squash,主干得到一条交付提交 S_A,它包含 A1 和 A2 的净变化。test 里仍然能看到两轮提测。如果 B 已经提前进入共享环境,直接拿 test 向 main 发 PR 就会包含 B。解决 test 上的冲突也留在测试侧;要独立上线的功能分支避免反向合入 test,以免把其他需求一起带走。
B 跟上 main 以后,test 仍可能无法快进
假设 B 此时还没有提测。B 原来有一条提交 B1,在自己的分支上 rebase 到 main 后,得到以 S_A 为父提交的 B1'。test 的最新提交则是 T2。虽然 S_A 和 T2 在这个例子里包含完全相同的文件,squash 创建的 S_A 只有 main 原 tip 这一个父提交。从 B1' 往回找,只能经过 S_A 回到原来的共同起点,找不到 T2。
可以先检查报错方向。目标是 test、来源是 feat/b 时,用 git merge-base --is-ancestor 检查快进条件。快进要求 test 的 tip 已经在 feat/b 的祖先链上。以下命令只读取远端跟踪引用,退出码 0 表示祖先关系成立,1 表示不成立。
git fetch origin
git merge-base --is-ancestor origin/test origin/feat/b
git log --oneline --graph --left-right origin/test...origin/feat/b
我用 Git 2.45.1 在隔离仓库里复现了这个状态。test 和 main 的 tree ID 相同,随后 B 在单独的文件上新增改动。在 test 上执行 git merge --ff-only feat/b,仍然得到下面的错误(省略了提示行)。
fatal: Not possible to fast-forward, aborting.
同一组提交用普通 merge 可以成功。如果普通 merge 也报 CONFLICT,再审查文件的三方合并结果。GitLab 的快进和半线性合入也会检查源分支是否基于当前目标分支,所以「B 刚 rebase 过」还得看它 rebase 到了哪个目标。
给共享 test 一条可以继续走的历史
共享 test 长期用来集成多个需求时,我倾向让它接受普通 merge。每次 main 有改动需要进入测试环境时,由维护者把 main 合进 test,把主干当前提交和原有测试记录接到同一段历史上。若 A 的两份改动内容相同,这次同步可能只新增一个合并提交。若 main 和 test 在同一处又有修改,就在同步分支上检查并解决冲突,验证后再合入。
共享 test 可以通过 PR 接收同步。先从 origin/test 建一条临时分支,在这条分支上 merge origin/main,再发往 test 的 PR。这个 PR 要保留合并的父提交关系,才能把 main 接进 test 的祖先链。同步后,main 的 tip 同时是 test 和 feat/b 的祖先,test 自己的新 tip 仍然不在 feat/b 的历史里。B 的提测 PR 继续走 feat/b → test,用普通 merge 把 B 带进去。发布仍用 feat/b → main,比较范围只包含 B。
如果项目只能快进,B 需要一条从 最新 test 出发的临时提测分支。以下以一条待提测的 B 提交为例,B_ONLY_SHA 换成经过核对、尚未进 test 的提交 ID。先确保工作树和索引干净。
git fetch origin &&
git switch --no-track -c qa/b origin/test &&
git cherry-pick B_ONLY_SHA &&
git diff origin/test...HEAD
核对差异后,提测 PR 走 qa/b → test,上线 PR 走 feat/b → main。如果 B1 之前已经进了 test,这次只移植新增的修复 B2。test 在等待期间往前走了,就按新 tip 准备候选并再次核对差异。这可以让 B 进 test,但 main 与 test 的长期同步仍要单独安排。若策略同时要求 test 严格线性,团队可以调整测试分支的合入方式,或者让流水线直接把合并候选部署到测试环境。
测试结果跟着当时部署的版本走
分支名会移动,验收记录应指向实际部署的提交或构建产物。在 A、B 这个例子里,可以按最终要发的组合决定什么时候重测。
| 实际发生的变化 | 上线前需要验证的版本 |
|---|---|
A 在 A1 之后追加了 A2 |
部署包含 A2 的版本,重测修复路径与受影响的回归场景 |
test 同时包含 A 和 B,但只发布 A |
验证 A 合入当前 main 后的候选,尤其确认 A 可以独立于 B 工作 |
审阅之后 main 又更新了,或合并时解决了冲突 |
以新的合入候选跑 CI,按实际改动补做相关验证 |
| 只是整理提交,最终文件树、构建输入和产物一致 | 可以复用已经验收过的产物证据。若平台按新 SHA 绑定必需检查,仍要让候选版本通过这些检查 |
GitLab 的合并结果流水线会用临时提交来验证源分支和目标分支的组合。只有一套共享测试环境时,也可以让流水线按需部署指定 PR 的候选构建,安排一个验收窗口。团队如果本来就是整批上线,则可以把已确认的 test 提交固定成发布候选,再验证整批和当前 main 的组合。发布期间 test 新接进来的需求可以留给下一批。
按需求部署时,每个 PR 可以独立提测
CI 可以在 feat/a 或其 PR 更新时部署 A,feat/b 的更新只刷新 B 的预览环境。GitLab Review Apps 就能按分支或 MR 提供临时环境。每次部署记录提交 ID 或制品版本,验收后分别从功能分支合入 main。若测试环境发布的是分支本身,而上线候选还会合入更新的主干,就按前面的验收条件补测受影响的组合。
多服务环境也可以用流量泳道把请求路由到该需求部署的服务版本。有些泳道在缺少对应服务时会回退到基线。即使 A 和 B 各有访问入口,也要核对它们是否共用数据库、队列或服务版本,以及测试数据会不会彼此影响。
总结
共享 test 需要能继续接收提测修复和主干更新,适合保留合入时的祖先关系。功能分支独立向 main 交付,主干可以只留一条 squash 提交。项目若只允许快进,就从实际目标分支准备对应的候选。无论测试环境怎么分配,验收都要落在实际准备发布的代码组合上。