一般来说,我们创建 PR 会有一个通用的工作流程:
- 创建一个分支,比如 feature
- 提交 PR
- 让别人 review
- 根据 review 意见修改
- 合并 PR 到主分支
但这个 feature 分支可能会有很多次提交,如何合并到主分支就成为了一个问题,GitHub 会给你三个选项:
Create a merge commit#
创建一个连接两个分支历史记录的 “合并提交”
我们可以在本地模拟图中这个过程,首先在主分支创建三个提交,使用 git log --oneline --graph
可以看到提交记录如下:
* b8472e4 (HEAD -> main) add c.txt
* 2db7159 add b.txt
* b5b52d8 add a.txt
然后在新的分支 git checkout -b feature
创建两个提交:
* 9770b82 (HEAD -> feature) add e.txt
* 4e65cc4 add d.txt
* b8472e4 (main) add c.txt
* 2db7159 add b.txt
* b5b52d8 add a.txt
切回主分支,使用命令合并:
git checkout main
git merge --no-ff feature
再次查看记录:
* 7da24fd (HEAD -> main) Merge branch 'feature' into main
|\
| * 9770b82 (feature) add e.txt
| * 4e65cc4 add d.txt
|/
* b8472e4 add c.txt
* 2db7159 add b.txt
* b5b52d8 add a.txt
如上,GitHub 在幕后使用 git merge --no-ff
的默认选项会创建一个连接两个分支历史记录的合并提交。
这种方法的优点就是保留了所有的历史记录以及各个时间节点,缺点就是多分支或者长期分支会一团乱麻。
Squash and merge#
将分支中的所有提交压缩替换为单个提交后合并
依旧来尝试本地模拟,首先 git reset --hard b8472e4
回到合并前:
* b8472e4 (HEAD -> main) add c.txt
* 2db7159 add b.txt
* b5b52d8 add a.txt
使用 git merge --squash feature
压缩合并,然后编写适当的提交消息。
最后主分支的提交记录如下:
* 52c548d (HEAD -> main) add d.txt and e.txt
* b8472e4 add c.txt
* 2db7159 add b.txt
* b5b52d8 add a.txt
可以看出这种方法的优点就是主分支历史清晰且线性,一个功能用一个提交解决,但是缺点就是丢失了合并以及各个独立提交的上下文信息,甚至没法知道这个分支到底合并没有。
另一种 Squash 的方法#
使用交互式 rebase 命令实现更细致的操作:
git checkout feature
git rebase -i main
选择 squash 将后面的提交融合到前一个提交:
pick 4e65cc4 add d.txt
squash 9770b82 add e.txt
编写适当的提交消息,此时 feature 分支提交记录变为:
* f37e60f (HEAD -> feature) add d.txt and e.txt
* b8472e4 (main) add c.txt
* 2db7159 add b.txt
* b5b52d8 add a.txt
最后回到主分支合并即可(当然你也可以用 cherry-pick 合并单独或组合的提交):
git checkout main
git merge feature
Bonus#
这种搞坏提交记录的操作怎么恢复?
git reflog
# 你会看到你做的所有 git 操作,找到搞坏的前一步 reset 即可
git reset HEAD@{index}
Rebase and merge#
所有提交都会被添加合并到主分支的 “顶部”
没图,但是可以从描述中了解到主分支最后的结果应该是:
恢复分支状态,然后执行以下操作:
git checkout feature
git rebase main
git checkout main
git merge feature
完成后主分支提交记录如下:
* 9770b82 (HEAD -> main, feature) add e.txt
* 4e65cc4 add d.txt
* b8472e4 add c.txt
* 2db7159 add b.txt
* b5b52d8 add a.txt
优点是主分支历史清晰线性,没有多余的合并提交,其他分支的提交历史也会直接保存在主分支记录里。
缺点与 squash 合并类似,并且更多小提交可能会影响对大局的注意力。
建议#
所有的选择都是 trade-off 的结果,应该权衡这几个关键的因素:
- 团队偏好
- 提交历史记录:完整还是清晰?
- 更小的提交还是整个功能的完整提交
- 有无长期运行的分支
- ...
我只有一个建议:多提交,多写 PR,不要堆积代码,更小的代码片段意味着更容易的 code review,更简单的单元测试以及更符合单一职责原则的代码。