原文地址
git rebase
因其“新手应当远离的Git黑魔法”的名号名声在外,但只要使用得当,其可以使团队开发变得无比轻松。本文将对比两个相似的命令:git rebase
与git rebase
来区分它们的使用场景,最终将“黑魔法”纳入自己的工作流中。
概述
首先你需要明白git rebase
与git merge
解决的是同一类问题,即都被设计来解决将一个分支与另一个分支整合的工作,所以它们可谓一体两面。
想象一下但你在专用分支上开发一个新的特性时,团队的另一个成员更新了主分支。这导致了及时记录上的一个分叉,这一场景对以Git作为协作工具的你我是再熟悉不过了。
假设这一提交与你在开发的功能有关,为了将提交纳入你的特性分支中,你必须在合并(merge)和变基(rebase)中作出选择。
选择合并
最简单的方式就是使用如下操作将主分支合并入特性分支中:
git checkout feature
git merge master
或者用一行操作:
git merge master feature
此时在特性分支上有了一次“合并提交(merge commit)”将两个分支的历史线中出现了一个共同节点如下图:
merge
是一个很优秀的命令,它是一个非破坏性的操作(历史线不改变结构)。已存在的分支并不会有任何变化。这能避免所有由rebase带来的潜在陷阱(后文将涉及)。
不过这也意味着,在每次相关上游变化时,你都要添加一条无关的合并提交。如果主分支更新活跃,使用合并会很大程度上污染你的特性分支历史线。虽然git log
命令可以一定程度缓解这一问题,但是其他程序猿将难以理解项目的历史线。
选择rebase
作为merge
的替代,你可以使用一下命令进行rebase
:
git checkout feature
git rebase master
这样所有的特性分支被移至主分支的末端,有效地纳入了主分支的新提交。但是rebase
命令重写了项目的历史线通过对先前分支的每一个提交都创建新的提交。
rebase
带来的主要好处是更清晰的项目历史线。首先,不再会有由merge带来的不必要的合并提交。其次,如上图所示,rebase命令将项目历史线变得理想的线形(从头到尾都没有分叉),这样,使用git log
,git bisect
,gitk
这样的命令都能轻松定位你的项目。
但是,对于rebase操作有两个需要考虑的点:安全性和可追溯性。你若不遵循“Rebase黄精准则”重写历史线将会给你的合作工作流带来灭顶之灾。还有一点需要注意的是:rebase并不存在像merge一样的上下文,这意味着你很难定位由上游变化而带来的特性的合并的位置。
交互式rebase
交互式rebase让你有能力去控制最终移动到分支末尾的提交。这比自动提交来的更强大,因其提供了对提交历史完整的控制。这通常被用于将某个分支合并进主分支之前,去清理杂乱的历史线。
通过加上在git rebase
中加入i
选项来开启交互式rebase:
git checkout feature
git rebase -i master
之后会打开一个文本编辑,其中罗列了将会被移动的所有提交:
pick 33d5b7a Message for commit #1
pick 9480b3d Message for commit #2
pick 5c67e61 Message for commit #3
这一列表展示的就是最终执行rebase
后分支。通过改变pick命令或重排顺序,你可以将分支历史线变成需要的样子。例如,如果第二行提交是为了修正了第一行提交的中德小问题,你可以将两者合为一条通过fixup
命令:
pick 33d5b7a Message for commit #1
fixup 9480b3d Message for commit #2
pick 5c67e61 Message for commit #3
当你保存并关闭了文件,git会根据你的指令去执行rebase
命令。项目历史线的结果如下:
去除掉非必要的提交后可以让你的特性分支历史线更容易读懂,这是简单的git merge
Rebase黄金法则
一旦你理解了rebase,最重要的事就是学会何时不使用rebase。Rebase黄金法则:永远不要在公开分支上使用git rebase
。
例如,想象一下如果你将你的特性分支rebase到主分支:
rebase将移动所有主分支上的所有提交到特性分支末端。问题是这只发生在你的仓库中。其他开发者依旧在最早的分支中开发。由于改变了最新的提交,Git会认为你的分支历史线与其他成员产生了分岔。
唯一能使两天分支同步的办法就是将它们合并在一起,这将会导致一条额外的提交与两组对相同变化的提交(初始分支上,你rebase的分支上)。不必说,这是一个极容易让人混淆的状态。
在你执行git rebase
之前,问一问自己,“其他人会看到这条分支?”如果会,把手从键盘上移开并且想一个无破坏性的方式来解决(例如git revert
命令)。此外,你可以尽情重写历史线。
强制发布
如果你要将一个rebase过的分支发布到远程分支上,Git会阻止你这样做,因为这会和远程主分支产生冲突。但是,可以加上-- force参数来强制发布:
# Be very careful with this command!
git push --force
该操作会用你仓库中的主分支覆盖远程仓库中的主分支,并且让团队中其他成员一头雾水。所以,请谨慎使用这条命令。
唯一需要强制推送的情况是在你推送了一个个人特性分支到远程仓库后,你完成了一次本地的清理。这好像就在说“哎哟喂,我不想要之前上传的特性分支了,用现在的版本替代吧”同时,需要确保没有人在之前那个版本的提交下开展工作。
工作流演练
Rebase可以被尽可能多/少地纳入你们现有的工作流中,这取决于团队对于它的态度。本节中我们将了解到rebase在分支开发的各个阶段带来的好处。
使用git rebase
的任何工作流的第一步是为每个特性创建一个专有分支。这将为你安全地使用rebase
提供必须的分支结构。
本地清理
将rebase
纳入你们的工作的的一个最好的方法是去清理本地在开发的特性。通过周期性的使用交互式rebase,可以确保你特性上的每一次提交是清晰且有意义的。这使得你可以去书写代码而不必担忧将其分割为一次独立的提交,因为事后还是可以修复的。
当调用git rebase
命令时,你需呀注意两个选项:特性的父分支(如主分支)或是在你的特性上更早的提交。我们参考交互式rebase一节中关于第一选项的例子。当你仅需要修改最近的几次提交时后一个参数是很有用的。例如,例如下面的命令创建一个只有最近三次提交的交互式rebase。
git checkout feature
git rebase -i HEAD~3
通过将HEAD~3
指定为新的“基”,你实际并不需要移动分支,你只是重写了三次提交。值得注意的是上述操作并没有把依赖的变化纳入特性分支。
如果你想要使用这种方法重写全部的特性,git merge-base
命令将能够帮助你找到特性分支之前的基。以下命令会返回之前基的提交ID
,用于之后执行git rebase
使用。
git merge-base feature master
这种交互式rebase的用法是将git rebase
引入你们工作流的一个好方法。其他开发者看到的只会是你最终的成品,其将拥有清晰,易于理解的特性分支历史。
但是再次重申,这只适用于专有特性分支,如果你和其他开发者同时在相同的分支上合作,那条分支就是共有的,重写历史线将不被允许。
交互式rebase用于清理本地提交的功能是不存在git merge
类的替代功能的。
将依赖的变化纳入特性中
在概述一节中,你了解到了如何使用git merge
/git rebase
将主分支上游的变化纳入一个特性分支中去。merge是一个可以保证仓库历史线的安全选择,同时,rebase通过将你的特性分支移至主分支末端来创建一条线形的历史线。
该用法与本地清理相似(可以同时进行),但是该过程从主分支纳入了上游提交。
请牢记,除了远程主分支外的任意远程分支的rebase都是完全合法的。这通常发生在团队协同开发同一特性,你需要将成员的修改纳入你的仓库时。
例如,如果你的伙伴John提交了一次修改,当你fetch远程分支从John的仓库中,你的分支就会像下图一样:
解决这一分岔你有两个选择:
1.用本地分支与John的分支进行merge:
2.使用rebase将你本地的特性分支移至John的分支末端:
注意这并未违背Rebase黄金法则,因为移动只发生在你的本地分支提交后,在此之前都是不变的。也就是说“添加我的修改在John的工作之后”。在多数情况下这比通过合并提交使远程分支同步的方法来得更为直观。
默认情况下,git pull
命令执行了一个merge,但是你可是通过加上--rebase
选项强制其使用rebase。
使用Pull Request
检查分支
如果你在代码检查阶段有使用Pull Request
,那么你需要避免使用git rebase
在你创建了pull request
后。一旦你执行了pull request
,其他开发者将会看见你的提交,这意味着这就是一个公共分支。重写历史线将导致Git和你的团队成员不能追踪到在此特性上补增的提交。
任何从其他开发者纳入的修改请使用git merge
而不要用git rebase
。
因此,通常的清理代码的做法是在执行你的pull request
前使用交互式rebase。
合并一个审核过的特性
当一个特性通过团队的审核后,在将特性整合进主代码库前,你可以选择将特性rebase到主分支末端。
该步骤和把上游修改整合进特性相似,但不同的是,由于你不能在主分支上重写提交(commits),你最终必须使用git merge
来合并特性。然而通过在rebase
之前执行rebase
可以确保迅速的合并,并且得到一条完美的线形历史线。这也提供了一个在pull request
阶段塞入任意补增提交的机会。
如果你还无法完全适应git rebase
你可以在短期分支上使用它。这时,当你不小心弄乱了你的分支历史线,你可以checkout
早前的分支,并且再次尝试:
git checkout feature
git checkout -b temporary-branch
git rebase -i master
# [Clean up the history]
git checkout master
git merge temporary-branch
总结
以上就是开始rebase你的分支前你需要知道的一切。如果你想要一条干净,清晰的历史线(没有非必要的合并提交),那么你需要使用git rebase
去吸纳别的分支上的修改而非使用git merge
。
不过,如果你想要保留完整的项目历史线,避免重写公共提交,那你需要贯彻git merge
。这两者都是完全有效的。但是至少现在你需要衡量一下使用git rebase
这个选择将带来的优势。