merging VS rebasing
本文原文出处:https://www.atlassian.com/git/tutorials/merging-vs-rebasing,我进行了翻译和归纳整理。
git rebase
命令对很多git新手来说就像个应该远离的魔法指令,但这个命令实际上用的好的话,可以极大提示程序员的开发体验。这篇文章中,我会比较git rebase
和git merge
,并说明在典型的git工作流中所有可能用到变基来合并的情况。
# 概念速览
首先需要明白,git rebase
和git merge
解决了一样的问题。它们都用来把一个分支的更改合并到另一个分支——只不过用不同的方法。
来看一种情况,你开始在一个新的分支工作,团队里另一个人更新了main
分支。这导致了大家很熟悉的分叉的git历史。
现在,假设在main
上新的提交和你的feature
是相关的。要合并main
上的新提交,你有两个选择:合并(merge)或变基(rebase)。
# 合并(merge)方式
合并这两个分支最简单的方式就是用下面的方法:
git checkout feature
git merge main
或者,可以变成一行:
git merge feature main
这种方法创建了一个feature
分支里的“新的提交”,把两个分支的历史记录联系在一起。现在的分支结构是:
合并是一个非破坏性的操作。现存的分支不会进行任何改变。这避免了使用变基时所有可能产生的错误(在下文讲解)。
然而,这也表明feature
分支会在每次你需要合并上游改动时产生不必要的合并提交记录。如果main
分支很活跃,这很可能会污染feature
分支的历史记录。虽然可以用高级git log
参数减轻这个问题,这会让其他人更难去理解项目的历史记录。
# 变基(rebase)方式
另一种合并的方式就是变基:
git checkout feature
git rebase main
这把整个feature
分支移动到main
分支的首部,实际上对main
上所有新的提交都执行了一次合并。变基重写了项目的历史记录,为原分支上的每个提交都创建了一个全新的提交记录,而不是创建一个新的提交记录。
变基最主要的优点就是它提供了更干净的历史记录。首先,它排除了合并方式下创建的不必要的合并提交记录。其次,由图可见,变基也能提供一个不存在任何分支的完美的线性历史。这让通过git log
、git bisect
等语句浏览项目历史时更简单了。
然而,这种方式对原始提交记录的安全性和可追溯性有影响。如果你不遵照变基的黄金法则,重写项目历史很可能是灾难。另外,变基失去了合并提交带来的信息。你看不到上游提交是什么时候合并到feature
分支的。
# 交互式变基
交互式变基提供了在提交被移动到新的分支后,修改这些提交的能力。这比自动变基更强大,它提供了对分支提交历史的完整控制权。特别的,它也被用来清理把feature
合并到main
时混乱的历史记录。
要创建一个新的交互式提交会话,在变基命令后添加-i
参数:
git checkout feature
git rebase -i main
这会打开文本编辑器,列出所有将要被移动的提交:
pick 33d5b7a Message for commit #1
pick 9480b3d Message for commit #2
pick 5c67e61 Message for commit #3
这个列表准确定义了在变基后分支的样子。通过改变pick
指令或重新排序这些内容,你可以让分支结构想怎么样就怎么样。比如,如果第二个提交修复了第一个提交的小问题,你可以通过fixup
命令把他们合并成一个单独的提交:
pick 33d5b7a Message for commit #1
fixup 9480b3d Message for commit #2
pick 5c67e61 Message for commit #3
当你保存并关闭文件时,Git会基于你的指令执行变基,让历史记录变成下面的样子:
像这样排除掉不必要的合并提交,可以让feature
分支的历史记录更容易理解。这是git merge
做不到的。
# 变基的黄金法则
当你明白变基是什么了,接下来最重要的就是学习说明时候不要用它。变基的黄金法则就是,永远不要在公共分支上使用它。
比如,想想下面的情况中执行把main
变基到feature
会发生什么:
变基把main
中所有的提交都移动到feature
的顶端。而问题就在于这只作用于你自己的git仓库。其他开发人员依然在原来的main
上开发。因为变基创建了全新的提交,Git会认为你的main
和其他人的产生了分叉。
同步两个main
的唯一方法是将他们合并,导致创建了一个包含原分支和你的变基分支的新的合并提交。这是非常让人费解的。
因此,在执行变基前,永远要问问自己,这个分支有其他人要使用吗?如果答案是有,不要使用变基,考虑像git merge
这种非破坏性的方式去提交更改。如果答案是没有,那就可以随意重写历史记录。
# 强制推送
如果你尝试推送已经变基的main
分支到远程仓库,这会和远程main
产生冲突,因此Git会阻止你。但你可以使用--force
强制推送:
git push --force
这会用你本地已变基的分支覆盖远程main
分支,这回让其他开发成员非常迷惑。因此,在使用这个命令的时候一定要三思,一定要在你知道你在干什么的时候才使用。
少数的需要使用强制推送的场景之一就是,你在推送一个私有分支后执行了本地的清理工作。就相当于,你完全不想推送这个分支原来的版本,要用现在的版本替换。需要再次强调,这依然要保证没有人在这个分支原始版本工作。
# 在工作流中的应用
这一节中,来看看变基能给不同情况下的开发带来的好处。
在任何工作流中,使用变基的第一步是为每个开发中的特性创建一个独立的分支。这会提供一个必要且安全的分支去执行变基。
# 本地历史记录清理
一个最好的变基方式就是执行本地正在开发中的特性分支。通过定期的执行交互式变基,你可以让你分支中的每个提交都更有意义。这允许你写代码的时候不担心把代码分散成一个个提交,你可以之后再合并他们。
当执行变基时,对新的“基”有两个选择:该分支的父分支(比如main
),或该分支的一个提交。我们已经在交互式变基一节中看到了第一种情况的应用。后一种选择在你只需要处理几个最后提交时很有用。比如,下面的指令开启了一次对最后三次提交的交互式变基:
git checkout feature git rebase -i HEAD~3
通过指定HEAD~3
为新的“基”,你不是真正在移动分支,你只是交互式地重写了在它之后的3个提交。另外,这不会把上游更改合并到feature
分支。
如果你想要用这个方法重写整个feature
分支,可以用git merge-base
命令。它可以找到feature
的起始提交记录点。下面的命令会返回起始提交记录点的提交ID:
git merge-base feature main
由于交互式变基只会影响本地分支,它的使用可以很好地把git rebase
引入你的工作流。其他开发者唯一能看见的是你最终的干净、能轻松阅读的feature
分支历史记录。
要再次强调的是,这只在私有分支生效。如果你在和其他开发者同时在一个feature
分支开发,那说明这个分支是公开的,你不能重写它的历史记录。
git merge
不能实现交互式变基能实现的本地历史记录清理功能。
# 合并上游更改到Feature分支
在概念速览节,我提到了featuer
分支可以使用git merge
或git rebase
。合并是安全保留整个仓库历史的选择,而变基通过将feature
移动到main
的顶端,创建一个线性的历史记录。
合并上游更改到Feature分支时,git rebase
的使用和执行本地清理差不多(而且可以是同时进行的),只不过还合并入了上游更改。
要记住,把分支变基到除了main
的远程分支是完全合法的。这种情况会在你和其他人同时在开发一个feature
分支,你需要把他们的更改合并到你的仓库里时发生。
比如,你和John都给feature
添加了提交,在拉取(fetch)来自John的远程feature
后,你的仓库可能看起来像这样:
你可以用你合并上游分支一样的方法解决这个分叉:要么把你本地的feature
和John的feature
合并,要么把你的本地feature
变基到John的feature
。
要注意的是,这没有违背变基的黄金法则,因为只有你的本地feature
的提交被移动了。之前所有的提交都没有被影响到。这就相当于,把你的改动加到John已经做过的部分后面。在大多数情况下,这比通过合并提交与远程分支同步更直观。默认情况下,git pull
命令执行的是合并,但你可以使用--rebase
参数强制通过变基合并远程分支。
# 在PR(Pull Request)时评审Feature分支
如果你的代码评审过程中使用了PR,务必避免在创建新的PR前执行git rebase
。一旦你创建了PR,其他开发者都会查看你的提交,这意味着你的分支成为了公共分支。重写提交历史会让Git和你的团队无法跟踪后续该分支的提交。
任何来自其他开发者的修改都必须使用git merge
。
因此,建议在提交PR前用交互式变基清理你的代码。
# 合并一个已通过的Feature分支
当一个feature
分支已经被你的团队通过时,你可以选择在使用git merge
把这个分支合并到main
前使用git rebase
把这个分支变基到main
的顶端。
这种情况和把合并上游更改入feature
差不多,但是因为你不能重写main
分支的提交记录,你必须最终使用git merge
来合并feature
。然而,在合并前执行变基意味着你假设该合并会是快进(fast-forwarded,即在合并feature
入main
时,main
没有新的提交),让最终能够有一个完美的线性提交历史。这也允许你在PR中压缩任何后续提交。
如果你对git rebase
不完全自信,你可以保证变基操作永远在临时分支上执行。这种情况下,如果你意外把feature
分支搞坏了,你可以回到原来的分支再试一次。例如:
git checkout feature
git checkout -b temporary-branch
git rebase -i main
# [Clean up the history]
git checkout main
git merge temporary-branch
# 总结
这是开始尝试变基前你需要知道的所有知识。如果你更喜欢一个干净、线性、没有不必要合并提交的历史记录,你应该在合并分支时使用git rebase
而不是git merge
。
然而,如果你想要保留完整的历史记录,不想冒着重写公共提交的风险,你依然可以继续使用git merge
。两种方式都是可行的,但你至少应该会权衡变基的利弊。
# 个人总结
变基的使用还是需要对它的作用原理有一个了解,才敢去真正的执行。在项目中,需要牢记:
- 永远不要把公共分支变基到其他分支
- 在变基前,可以创建一个临时分支来毫无顾忌地尝试