GitHub Merge Pull request 的三个选项说明
讲解
Github上的 Merge pull request 提供了 3 种 merge 方法(注意没有平常使用时快进式的merge):
Create a merge commit:GitHub 的底层操作是 git merge –no-ff。feature 分支上所有的 commit 都会加到 master 分支上,并且会生成一个 merge commit。这种方式可以让我们清晰地知道是谁做了提交,做了哪些提交,回溯历史的时候也会更加方便。
--ff(fast-forward)
: 是快进式合并, 也是我们日常使用merge的默认合并方式。 (缺点是如果删除分支, 就会丢失分支信息, 比如你在分支上的stash链表)。--no--ff
: 是非快进式合并, 也就是不使用快进式合并的意思, 也就是为什么单一的线上分支中会发现一些相同内容的不同提交的原因。 因为它是非快进式的, 在合并时是一定会产生一个新的提交的 (也就是说, 如果你pr中有多个commit, 他会将分支的所有commit都加到线上分支中, 然会生成一个pr中所有commit的squash提交记录)。 这种方式可以让我们清晰地知道是谁做了提交,做了哪些提交, 以及谁处理的pr,回溯历史的时候也会更加方便。不过如果你使用了一些ui工具去看这个线上主分支时, 可能会发现它并不是一个单一的直线。- 那么不使用自带的log命令行ui, 而用vscode的插件ui演示下, 如下图示例(图中均为通过ui显示单一分支式所看到的结果<注意其中的分叉只是假象,是非快进式合并带来的必然结果,新人需要理解这一点>):
- 如果单一在ui查看线上主分支时, 出现分叉现象, 那你就要意识到这个分叉很有可能来自于一个线上的pr处理。
- 如果您需要线上分支达到快进式合并的结果(如果您处理的是非自己的pr, 不用因为分叉强迫症来摧毁历史记录(注意为了贡献者着想)), 不妨使用第三个选项(也就是Rebase and merge这个选项), 来保证线上分支不分叉。(注意这是一个rebase)
Squash and merge:GitHub 的底层操作是 git merge –squash。Squash and merge 会使该 pull request 上的所有 commit 都合并成一个 commit ,然后加到 master 分支上,但原来的 commit 历史会丢失。如果开发人员在 feature 分支上提交的 commit 非常随意,没有规范,那么我们可以选择这种方法来丢弃无意义的 commit。但是在大型项目中,每个开发人员都应该是遵循 commit 规范的,因此我不建议你在团队开发中使用 Squash and merge。
如果您确实遇到了没有遵守 commit 规范的, 多个提交pr, 您可以使用这个选项处理它, 将他们合成一个提交(此时没必要记录pr的作者)(更没必要保留不规范的commit记录)。 由于此时是快进式merge, 请注意log的约定式提交规范。
Rebase and merge:GitHub 的底层操作是 git rebase。这种方式会将 pull request 上的所有提交历史按照原有顺序依次添加到 master 分支的头部(HEAD)。因为 git rebase 有风险,在你不完全熟悉 Git 工作流时,我不建议 merge 时选择这个。但如果您就是项目的负责人或者是主要开发者和管理者, 您可以使用这个来使单一分支更清晰。
(前提是您当前pr的所有提交都符合您心中的规范, 即没必要Squash或是专门生成一个非快进式的merge提交, 没必要记录pr的作者和处理者(因为就是您本人)。)
示例

对于全pr, 我们可以使用checkout+分支uuid的方式, 配合 push时的 head:开发分支名, 来进行分批次的pr提交。 就算, 少了或者多了都没关系, 因为我们基于pr的提交, 可以随时通过变基操作 或 刚提得操作 配合 强制提交来修改pr, 这也是在pr中常见的操作, 因为如果你的pr不符合维护者的心意, 可能会被拒绝合入主分支的。
![]()
接下来对方需要的操作是
先通过rebase -i 在本地的aaa分支中逆转前两次与后两次提交的顺序
执行
git rebase -i head~4
后, 按下方图片变基* 处理变基带来的冲突。由于不需要更改commit提交信息, 因此处理中途后我们依次执行`git commit --amend --no-edit`来提交对冲突的修改。(由于我们所做的操作是顺序交换, 不存在修改, 因此全部使用`git rebase --continue`即可)(tips: 具体根据前一步骤所给的提示来就好) * 然后执行`git rebase --continue`, 同意继续变基(若再次遇到冲突, 则返回上一步处理) * 前两步中具体用哪个, 可完全参照每一步骤后git给出的提示来选择进行。直到变基全部成功。 >
- 然后通过checkout, 到达对应的前两次提交的位置, 在该位置执行强制push操作
git push origin head:aaa -f
此时由于github的pr是基于 aaa 分支的, 因此aaa分支被强制push发生变化, 就可以使得pr中的内容发生变化, 进而达到目的。
![]()
我看到了这个pr已经满足我的要求后, 我就可以将其合并至主分支, 然后等待他的第二个pr了。
在对方看到我完成对第一个pr的合入操作后, 就会在本地做以下操作, 以生成第二个pr
- 先checkout, 切换到aaa分支的实际位置
- 在当前aaa分支的实际位置, 进行
git push origin aaa
- 在远端查看, aaa分支超出main分支2个提交, 满足了pr生成的基础, 点击生成新的pr(即生成第二次pr)
最后, 我合入他的第二次pr。
至此, 我完成了对该用户pr的审核。 结果是全部允许合入。(当然, 审核结果如果是拆分后只要第一次pr(极端模拟沟通用户增删改), 则结果就是部分允许合入了哈哈, 总之, 对应用户来说没有全部部分之分, 允许了就是允许了, 他的pr终归是被我采纳了)
总结
实际使用中, 若您完全相信自己的实力以及github这个平台的话, 可以在主分支规则配置中,配置强制性的单一分支策略(tips:禁止处理pr时的非快进式的合并选项)。
ok, 那么接下来的工作就变得简单起来了, 首先要明确的是这是我们自己的项目, 要以自己为基准。
虽然在自己遵循约定式提交的情况下, 直接基于主分支操作也不是不行(不过我还是不推荐), 但是我更推荐即使只有自己在开发项目, 也通过pr来进行与主分支的合入(尽管每次pr的处理都采用rebase的方式)。
(也就是虽然拥有push主分支的能力, 但是我不用, 目前github找不到禁止自己对主分支的普通push规则, 否则我一定限制自己对自己项目的这个能力)删除这一句的原因是github实际拥有对禁止自己对主分支的普通push规则, 而我设置规则后, 还能以外push成功的原有是因为, 我所在的进行push的位置, 实际上已经在github的pr上了, 因此造成了以外的push成功,并且此bug还导致了pr被自动关闭。(准确来说, 我们意外的使用了快进式提交, 将pr合并到了主分支——pr的默认选项中没有这种方式)
对于复杂的业务功能, 我们的feat, 尽可能按照开发过程的代码功能来划分, 里面包含代码功能的不断fix也无所谓, 甚至是test, 因为只有这样才能够保证我们的开发速度。但pr时, 尽可能按照squash的方式进行合入(包含刚才的所有feat,fix以及test), 而squash所产生提交的feat, 按照我们的业务功能命名即可, 这样也方便后期修复线上bug时给fix命名以及版本发布, 以及test的命名。
比如对此功能的某个bug的修复。 对此功能的某个部分test的更新或重构(使用test标签, 不计入changelog)。
对于简单的业务功能, 则无需以squash的方式合入, 直接通过rebase即可。
那么最重要的好处来了, 按照此种提交方案, 完全可以开辟一个新分支, 专门用来做非快进式更新。 并且也不会因为多了这个分支而多出太多的工作量, 因为pr的方式完全同步, 只需要在处理好这个分支后, 在到主分支中进行一次squash就好了。成本低到只需要点击两次pr创建和多一次合入操作, 但带来的收益却是巨大的, 这个分快进式更新的分支为我们项目提供了脱离平台生存的能力。 此时, 你大可把非快进式合并的分支作为传统意义上的主分支, 而把实际的单一分支策略的主分支当作发布分支或者说是版本分支。
值得注意的是, 两个主分支的版本号一定是不一致的, 但是不影响任何代码提交推送管理, 以及潜在风险层面的, 未来有可能发生的潜在的回溯版本操作。
还有对于tag, 我们需要以主发布mian分支为基准来打tag, 再在之后找到我们的开发主分支, 手动复制由发布分支得到的changelog.md文件以及相关的版本文件(如前端可能会涉及package.json文件)到我们的开发分支, 在对应的位置提交打tag, 因此对于tag的维护也是需要两套的
团队的其它成员, 也是如此(和自己差不多, 但不允许拥有直接push主分支的能力), 磨合程度高了之后, 可以配置一个互相审查代码的机制, 保证每次合入主分支的代码, 都要有两人左右审查。
以上两点的好处有很多, 而且还可以在审查过程中发现问题并处理问题后再合入主分支, 提高主分支的安全性, 甚至对于一些需要大幅度更改的情况, 我们也可以很方便的基于开发分支的pr做变基操作后, 再进行与主分支的合并。
缺点也有, 就是每次都需要删除原有分支, 因为不论是squash还是rebase, 本质上都是放弃了原有的提交的uuid号码, 在主分支产生新的uuid的commit。
github的
pr系统
和issues系统
实际上使用的是同一组计数器。而对于未加入团队的社区pr, 由审核人决定是否采用squash方式向主分支合并(大多数情况下都建议这样去做), 这样能提高我们主分支的可读性, 同时, 基于github平台关于此pr的整个评审处理流程也都会保留, 我们也无需担心历史丢失问题。
对于具体bug问题的解决, 也都无关是否为团队成员所提pr, 同样根据功能规模来判断适合使用哪种合入方式。
对于issues的处理, 就按照
close #数字
的形式就可以了, 至于是feat还是fix还是re.., 我们的commit的日志主题中会有体现, 无需在此处体现。
tips: 如果你的代码托管不局限于单一平台(跨平台的仓库同步不同步pr), 这里还是建议您尽量将历史保留在主分支中。 那么此时, 建议您放弃主分支的单一分支策略, 并采用非快进式合并来管理主分支, 将pr的信息, 尽可能体现在非快进式提交所产生commit的log描述中。
心得
没必要纠结于最完美的主分支, 我们只需要尽可能做的每次提交的规范, 以及保证整个项目的可维护性, 就足够了。 也就是说, 我们真正需要关注的是当下的代码和commit以及未来的commit, 而对于历史已经产生的commit, 是有着边际效应递减的特性的, 越古老的提交越健硕。