Merging VS. Rebasing
对于初学者来说git rebase
命令就如同Git当中的巫术一样,应该被敬而远之。但是它在小心使用情况下,确实可以让开发团队更加方便。这篇文章中,将对git rebase
和git merge
两个命令进行比对,并辨别所有潜在的基于rebasing的Git工作流。
概念简述
首先我们要知道git rebase
和git merge
解决的问题是类似的。它们都被设计出来用于将一个分支的变更合到另一个分支上——只是它们的方式不一样。
想想当你新开一个分支用来开发新特性,而另一个程序员在master
分支上提交了新的commit会发生什么。这个结果就是大家把Git作为一个合作开发工具时经常遇到的分叉历史(fork history)情形。
[图片上传失败...(image-67c7ab-1533606429903)]
现在,假设master
分支上的新commit和你开发中的新功能是有相关性的。为了将新的commit归入你的feature
分支中,你面临两个选项:merging或rebasing。
Merge选项
最简单的选择就是将master
分支合并(merge)到新功能分支(feature)中,命令如下:
git checkout feature
git merge master
或者你可以直接写个单行命令
git merge master feature
这个操作会在feature
分支中产生一个将两个分支缠在一起的新的“merge commit”,你的分支结构将会变成这个样子:
[图片上传失败...(image-81137e-1533606429903)]
Merging操作因为其无危害行,所以是一个非常不错的操作。已经存在的所有分支都不会改变。这避免了所有rebasing操作会产生的隐患(下面会讨论)。
另一方面,这也表示你的”feature“分支在你每次需要归并新的跟改后,会出现一个无关开发的”merge commit“。当”master“非常活跃时,这相当会污染你的feature分支的历史。虽然这可能可以用过使用高级的git log
选项缓解这个问题,但它依旧会导致其他开发者难以理解项目的开发历史。
Rebase操作
作为一种merging的替代品,你可以用一下命令将feature
分支rebase到master
分支上:
git checkout feature
git rebase master
这将会移动整个feature
分支到master
分支的尖端上,使得master
分支的所有新commit都有效的归并进来。但是,和使用merge commit不同,rebasing会通过为原来分支的每个commit创建新的含有标志的commit重写项目的历史。
[图片上传失败...(image-989052-1533606429903)]
Rebasing最大的有点就是你会得到一个更干净的项目历史。首先,它没有git merge
必须有却不需要有的merge commit。第二,就想你看到的示意图那样,rebasing的结果是一个完美的线性项目历史——你可以一直保持从项目的尖端开始开发新功能而不会出现分叉。这将更容易的让你的项目使用像git log
、git bisect
和gitk
这写命令。
但是,对于这个新的commit历史,有两个需要权衡的事项:安全和可追溯性。如果你不遵守Golden Rule of Rebasing,重写项目历史会成为你合作开发中潜在的灾难性隐患。所以,除非不重要,rebasing会失去merge commit提供的上下文关系——你看不到上游更改什么时候归入你的feature分支。
Interactive Rebasing
当将commits移动到新的分支时,Interactive rebasing会给你一个改变commits信息的机会。这个功能比自动rebase更加有用,因为它提供你完全掌控分支的commit历史的能力。最典型的用法就是在将一个feature合并到`master前,用来清理feature分支混乱的历史提交。
开始一个interactive rebase操作需要加上一个i
选项在git rebase
命令中:
git checkout feature
git rebase -i master
然后会出现一个罗列所有需要移动commit列表的编辑器:
pick 33d5b7a Message for commit #1
pick 9480b3d Message for commit #2
pick 5c67e61 Message for commit #3
这个列表明确定义了这个分支在rebase操作确认后的样子。通过改变pick
命令或重编入口,你可以随心所欲的改变这个分支的历史。举个栗子,如果第二个commit修改了第一个commit中的一个小问题,你可以通过fixup
命令将它单独固话成一个commit:
pick 33d5b7a Message for commit #1
fixup 9480b3d Message for commit #2
pick 5c67e61 Message for commit #3
当你保存并关闭文件,Git会根据你的说明做rebase操作,项目历史的结果将会如下:
[图片上传失败...(image-53d4bf-1533606429903)]
排除了无意义的commit后,你的feature
分支的历史变的更容易理解了。这种活git merge
是做不到的。
The Golden Rule of Rebasing
一旦你理解了说明是rebasing,接下来最重要的事情就是知道什么时候不应该使用它。git rebase
黄金规则是绝对不要在公共分支使用它。
举个栗子,想想当你将master
分支rebase到你的分支会发生什么:
[图片上传失败...(image-f15516-1533606429903)]
rebase操作将所有在master
上的commit移到了feature
的尖端。问题是,这个只发生在你的repository中。其他程序员依旧在原来的master
分支上工作呢!在rebasing出现的新commit出现后,Git会认为你的master
分支的历史和其他人的发生的分叉。
唯一将两个master
分支同步到一起的方法只有将它们合并(merge)回来,结果就是出现一个额外的merge commit和两个拥有相同更改的commit集合(一个是原来存在的,另一个是从你的rebased分支来的)。毫无疑问的说,这个一个非常让人迷惑的境地。
所以,在你运行git rebase
命令前,你要总是反省自己,”其他人会看到这个分支吗?“如果会,那么把你的咸猪手从键盘上移开,然后想一个没有破坏性的方式做你的更改(比如,git revert
命令)。如果不会,那么你安全的重写历史吧,怎么开心怎么写(:
Force-Pushing
如果你试图将rebase过的master
分支push到远程仓库上,Git将会阻止你这样做,因为这和远程master
分支时冲突的。但是,你可以通过强制push通过加上--force
标识,像这样:
# Be very careful with this command!
git push --force
这样会用你的本地仓库被rebase的master
分支覆盖远程master
分支,这样会使团队中的其他人感到迷惑(所有人都会遇到冲突!)。所以,一定要非常小心的使用这个命令,除非你非常明确的知道你在做什么。
唯一你可以这样force-pushing的情况是当你完成一个本地清理后,你想将这个私有的feature分支push到远程仓库中(例如,备份目标)。这就像是在说,”妈的,我真的不想将这个原来版本的分支push上去。将当前的版本替换上去。“再次声明,重要的是要保证没有其他人在原来的版本的分支上进行新功能分支的开发。
工作流程演练
Rebasing操作根据你们团队当前存在的Git工作流程适当的加入使用。在这个部分,我们来看看rebasing在新功能开发中可以提供哪些好处。
任何工作流中要支配rebasing操作的第一步就是为每个新功能创建一个献祭分支。这给予了你分支结构安全使用rebasing的基础条件:
[图片上传失败...(image-7b02b8-1533606429903)]
本地清理
将rebasing操作归入你的工作流中的其中一种最优方式是一边清理本地、一边开发新功能。周期性使用interactive rebase,你可以保证你的每一个commit在你的feature
分支中是清晰且有意义的。这可以让你在写代码是不需要去担心那些跳脱的commit——你可以在之后修复它们。
当使用git rebase
时,你有两个选项可以用来base:新功能分支的父分支(例如,master),或者只你的新功能分支的之前的commit。我们在Interactive Rebasing 章节看过第一个选项的例子了。后一个选项在你只要修复最近几个commit时显得更优秀。举个例子,以下命令表示对最近的3个commit进行interactive rebase操作。
git checkout feature
git rebase -i HEAD~3
通过具体的HEAD~3
作为新的基点,你不需要真的移动分支——你只要interactively重写3个基于它的commit。注意这将不会归入feature分支的上游更改。
[图片上传失败...(image-5e1042-1533606429903)]
如果你想用这个方法重写feature分支的全部commit,git merge-base
命令可以方便的找到feature分支的原来基点。使用后接下来会返回原来基点的commit ID,然后你可以用来进行git rebase
操作了:
git merge-base feature master
这种interactive rebasing的使用方式由于它只影响本地分支,所以它也是一个非常好的在你工作流中使用git rebase
的理由。其他程序员只会看到你已经完成的开发功能拥有一个干净、简单引出新功能分支的历史。
但是在此声明,这只能用于私有的新功能分支。如果你和其他程序员在相同的新功能分支合作开发,这个分支就是公开的,公开的分支就不允许你重写历史。
没有任何git merge
操作可以替代用interactive rebase
清理本地commit。
将上游更改归入feature分支
在Conceptual Overview 章节中,我们看到如何用git merge
或git rebase
将上游分支归入master
分支。Merging是一个保护你仓库中全部历史的方式,rebasing则是用将feature分支移动到master分支尖端的方式建立一个线性的历史。
这个使用git rebase
的方式和本地清理差不多(两者可以一起使用),但是过程中,它是将master上的上游commit归入其中。
要牢记的是,rebase到远程分支不能是master这一铁律。这可以发生在和其他程序员共同维护同一个feature分支时,然后你必须将他们的更改全部归并到你的仓库中。
举个例子,让你和另一个叫John的程序员一起增加commit到feature分支,你的仓库可能看上去像如下情形,在feaching远程John的仓库的feature分支之后:
[图片上传失败...(image-bedec7-1533606429903)]
你可以解决这个分叉就像你合并master分支的上游更改一样:不论你merge本地feature
和john/feature
,还是rebase你的本地feature
到john/feature
尖端。
[图片上传失败...(image-babae2-1533606429903)]
[图片上传失败...(image-e8bf7d-1533606429903)]
注意,这个rebase不违反Golden Rule of Rebasing,因为只有你的本地feature
commit被移动了——所有早于它的都没有被碰。这就像是说,”把握的更改加到John已经做好的那部分上。“大多数情况下,这比通过合并与远程分支同步更直观。
默认的,git pull
命令会执行一次merge,但你可以强制用rebase合并远程分支,只要你加一个--rebase
选项即可。
用Pull Request审查Feature分支
如果你用pull request作为你审查代码过程的一部分,那么在发起pull request后,你需要避免使用git base
。在你发起pull request后,其他程序员将可以看到你的commits,这就意味着你的分支是一个公共分支了。重写它的历史对Git来说是行不通的,而你的队友会跟踪后续的提交增加新功能。
任何其他程序员的更改都应该用git rebase
合并,而不是git rebase
。
以此,一般在提交pull request前使用interactive rebase 清理你的代码是一个好的想法。
合并一个认可的Feature
当一个feature已经被你的团队认可,在使用merge将feature合并到主工程代码之前,你拥有使用rebasing将feature放到master分支尖端的选择。
这个场景和将上游更改归入feature分支类似,但是你不被循序重写master分支的commit历史,你最终就只能用git merge
来合并feature分支。然而,在merge只用使用rebase,你可以保证merge可以在完美的线性历史中,快速进行。这同时也给了你压制在你发起pull request启发发生后续commit的机会。
[图片上传失败...(image-fd1ebf-1533606429903)]
如果你依旧无法完全对git rebase
感到舒服,你可以总是通过rebase一个临时分支的方式。通过这种方式,当你意外的弄乱了一个feature的历史,你可以checkout原分支,然后再来一次。举个栗子:
git checkout feature
git checkout -b temporary-branch
git rebase -i master
# [Clean up the history]
git checkout master
git merge temporary-branch
总结
这就是所有你准备对你的分支使用rebasing需要知道的东西。当你希望一个干净的、线性的历史,没有不需要的merge commit,你就应该在从其他分支合并更改时尝试git rebase
替代git merge
。
话说回来,如果你想要保证你项目有一个完整的历史,避免任何重写公共commit的风险,你就继续使用git merge
吧。两个选择都是完全有效的,但至少,现在你可以选择利用git rebase
的好处。