跳到正文

我所理解的 Git 最佳实践(4):在 GitHub 和 GitLab 配置 PR 的合入方式

我所理解的 Git 最佳实践

  1. 从工作区到一条可审阅的提交
  2. 什么时候用 rebase,什么时候用 merge
  3. 利用 amend 和 rebase 保持提交简洁
  4. 在 GitHub 和 GitLab 配置 PR 的合入方式(当前)
  5. 共享测试分支与按需求部署
  6. 弄清工作树、嵌套仓库和远端的归属
  7. 没有远端时怎样用 Git 维护项目
  8. 为什么切到 orphan 分支后源码不见了
  9. 先量清仓库体积,再决定怎么迁移
  10. 用 Node.js 检查 Git 路径,提前发现 Windows 检出问题

一条功能分支上有三条提交,评审已经通过。合入时,有的团队希望把这三条作为一次完整交付,有的团队要保留每条提交,还有的团队希望在主干上一眼看出哪几条是一批。配置 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 时,每项工作单独开主题分支更容易隔离差异。

Fork 和同仓协作使用不同的写权限边界

团队可以在分支规则里约定 main 需要 PR、通过约定的审阅与 CI,并限制直接推送和强推。GitHub 的仓库级选项和目标分支的规则要彼此兼容,否则页面即使有 PR,也可能没有可用的合入方式。GitLab 的合并方式在项目级配置,如果项目还维护共享 test,也要把反复提测和主干同步的场景算进去。 分支的推送与合入权限由 GitLab 的保护规则单独管理。

总结

一次性功能分支可以在合入时 squash,主干每次新增一条交付记录。需要长期追踪两边开发线时,合并提交可以保留父子关系。选好目标历史的形状之后,还要确认分支保护和流水线能支持团队实际走的合入路径。