我所理解的 Git 最佳实践(4):在 GitHub 和 GitLab 配置 PR 的合入方式
一条功能分支上有三条提交,评审已经通过。合入时,有的团队希望把这三条作为一次完整交付,有的团队要保留每条提交,还有的团队希望在主干上一眼看出哪几条是一批。配置 GitHub 的 Pull Request(PR)或 GitLab 的 Merge Request(MR)之前,可以先决定主干要留下哪种记录。
GitHub 在仓库里开放合入选项
打开仓库的 Settings → General → Pull Requests。三种方式可以同时启用,也可以只启用一种。假设 PR 有三条非空提交,与当前 main 无冲突,主干也还没有等价的补丁。
| GitHub 合入方式 | main 新增的记录 |
适合的交付 |
|---|---|---|
| Merge pull request | 保留三条原提交,再加一个合并提交 | 需要原提交 ID,以及一次明确的合入边界 |
| Squash and merge | 一条普通提交,包含这次 PR 的净变化 | 三条提交都属于一次交付,包含 CI 或提测修正 |
| Rebase and merge | 逐条把功能改动重放到 main,在本例中生成三条新提交 |
三条各自有审阅价值,希望主干保持线性 |
Rebase and merge 接在目标分支末尾,目标分支原有提交仍是新 tip 的祖先。GitHub 会给重放的提交分配新 SHA。快进合并也能形成线性历史,但会直接移动目标分支指针、保留源分支的原提交 ID。启用了合并队列时,合入方式由队列配置决定。
GitLab 分别配置合并方式和 squash
打开项目的 Settings → Merge requests。Merge method 决定是否保留合并节点,以及源分支需要满足的祖先关系。Squash commits when merging 单独决定是否压缩提交。Do not allow 禁止 squash,Allow 默认关闭、可手动开启,Encourage 默认开启、可手动取消,Require 则强制开启。
| GitLab 合并方式 | 关闭 squash | 开启 squash |
|---|---|---|
| Merge commit | 原提交和一个合并节点 | 一条 squash 提交和一个合并节点 |
| Merge commit with semi-linear history | 可快进时才合入,保留原提交和一个合并节点 | 同样要求可快进,保留 squash 提交和合并节点 |
| Fast-forward merge | 可快进时前移目标指针,保留原提交 | 合入后只新增一条普通提交 |
所以,「每个 MR 在 main 只新增一条普通提交」可以选 Fast-forward merge 加 Require。半线性和快进方式都要注意基线。目标分支往前走以后,即使准备 squash,GitLab 也可能要求先更新源分支。项目版本和设置支持自动 rebase 时,平台可以在服务端准备新的合入结果;GitLab 文档明确说这一步不会在 rebase 结果上重跑流水线。项目要在合入前验证最终组合时,可以按版本与套餐评估合并结果流水线和合并列车。
在开发时整理,还是合入时 squash
本地用交互式 rebase 压缩提交,更新的是功能分支。平台合入时 squash,则把同一项功能的净变化写成目标分支上的一条普通提交,源分支上的进度提交还在。两种方式都能让主干只保留一条交付提交。用 GitLab 的 Merge commit 方式时,平台还会额外留下一个合并节点。
如果项目不开 squash,希望一次需求只带入一条功能提交,可以在个人分支上按第 3 篇的办法整理。如果项目会在合入时 squash,开发阶段的进度提交通常可以留着,合入前把最终提交标题写成变更结果,再补上仍有用的兼容条件。
GitHub 的 squash 默认说明取决于仓库设置和提交数。默认设置下,多提交 PR 用 PR 标题和提交列表,单提交 PR 用该提交的标题和正文。仓库也可以改用 PR 标题和描述等格式。GitLab 的 squash 提交模板默认只有 MR 标题,项目可改模板,合入者也可编辑最终说明。
给长期分支保留能继续合并的历史
对普通业务需求,我偏向让一个 PR 对应一个交付单元,最终留下一个说得清的提交。若 PR 里有两个可以各自上线的需求,先拆成两个 PR。若合入的是长期发布线,或者同一条分支还要反复合进同一个目标,保留合并节点通常更方便追溯。squash 会把净变化带过去,源分支的旧提交却不在目标分支的祖先链上。继续用原分支开 PR,旧提交可能再次出现在比较范围里。普通主题分支完成后,下一项工作可以从更新后的主干新建分支。
本地也可以用 git merge --squash 理解这种结果。它在当前分支准备工作树和索引,再由 git commit 创建普通提交。
分支规则约束谁可以合入
Fork 和同仓协作的区别先体现在写权限上。开源贡献者通常推自己的 fork,再向上游发 PR。同仓协作则允许成员直接推主题分支,主干通过分支规则限制直接推送、强推和合入权限。使用 fork 时,常用 upstream/main 作为同步基线,向 origin 推送 PR 的源分支。两个 remote 的实际地址可以用 git remote -v 核对;并行提交多个 PR 时,每项工作单独开主题分支更容易隔离差异。
团队可以在分支规则里约定 main 需要 PR、通过约定的审阅与 CI,并限制直接推送和强推。GitHub 的仓库级选项和目标分支的规则要彼此兼容,否则页面即使有 PR,也可能没有可用的合入方式。GitLab 的合并方式在项目级配置,如果项目还维护共享 test,也要把反复提测和主干同步的场景算进去。 分支的推送与合入权限由 GitLab 的保护规则单独管理。
总结
一次性功能分支可以在合入时 squash,主干每次新增一条交付记录。需要长期追踪两边开发线时,合并提交可以保留父子关系。选好目标历史的形状之后,还要确认分支保护和流水线能支持团队实际走的合入路径。