大数据

Git: Merge vs Rebase

本文翻译自atlassian的git教程,原文地址是 merging-vs-rebasing

前言

git rebase命令一般都不推荐新手学,但是如果小心使用,这个命令确实可以让开发过程省力不少。这篇文章将会比较git rebasegit merge,以及讲述git rebase命令使用的场景。

基础概念

git rebasegit merge解决的是同一个问题。这两个命令都是用来将一个分支的内容合并到另一个分支,但是实现的方式却大相径庭。

考虑这样一个场景,你从主分支上切出一个分支来开发新的feature,同时,你的同事向master分支提交了一些commit,这在使用git管理开发项目的时候是很常见的,现在代码树是下图的样子。

假设master上提交的新代码和你正在开发的新feature是相关的。现在要将master上新的代码合并到你的feature分支上,有两种方式可以做到这一点:merge和rebase。

Merge的方式

将master上的代码合并到新分支上最简单的方式是这样

git checkout feature
git merge master

或者可以通过一条命令解决

git merge master feature

上面的操作会在feature分支上重新生成一个merge commit,现在的代码树是像下面那样

merge并不会破坏之前的提交记录,这对开发者来说是很好的,因为这样避免了出现意想不到的问题。

但是在另外一方面,这也意味每当需要合并别人的代码的时候,你的feature分支都会多一条多余的commit记录。如果master分支更新很频繁,这会让你的提交历史显得很冗长。

可以通过git log的设置过滤掉多余的提交记录,但是这样的提交记录无疑会让其他的开发者非常困扰。

Rebase的方式

另外一种方式是用rebase命令来改变你当前feature分支的提交历史,通过下面的命令可以做到这一点

git checkout feature
git rebase master

拿之前的例子来说,rebase操作会做下面的事情:回到feature和master分支的共同祖先,根据feature的历次提交对象,生成一系列文件补丁,然后以master分支最后一个提交对象为新的出发点,逐个应用之前准备好的补丁文件,最后会生成一个合并提交对象,从而改变了feature分支的提交历史,新的代码树如下图所示。

rebase操作主要的好处是让提交历史看起来更干净。首先,它去掉了merge操作会带来的不必要的commit记录。第二,正如上面看到的,它能让项目开发历史成线性记录。开发者能够顺着代码提交历史追踪一个feature的开发流程。这让git loggit bisectgitk操作变得更为简单。

但是这样重写commit记录在两个方面存在缺陷:安全性和可追朔性。如果不遵循一些规则,重写项目提交历史对项目的其他组员来说可能是一场灾难。还有一点,rebase不会有merge操作提供的上下文记录,你无法得知这些主分支上的改动是什么时候合并到feature分支上的。

交互式的rebase

交互式的rebase能在commit被移到新的分支上时对它们做出改变。这比自动的rebase更有用,因为在操作过程中能够完全掌控分支的提交历史。一般用这个功能来清理比较杂乱的提交记录。

-i 参数提供了这个功能

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命令或者是改变列表的顺序,能够让分支的提交历史变成你想要的样子。举个例子,如果第二个提交修正了第一次提交的一个小bug,那么可以让这两次提交变成一次提交,用fixup命令

pick 33d5b7a Message for commit #1
fixup 9480b3d Message for commit #2
pick 5c67e61 Message for commit #3

保存后,Git将会根据上面的信息执行rebase操作,项目的提交历史会像这样

像这样消除不重要的commit记录,会让开发这个feature过程更容易理解,这个功能是merge操作做不到的。

rebase的使用守则

了解完什么是rebase后,最重要的事情就是知道在什么情况下不使用这个命令。最需要记住的是,一旦分支中的提交对象发布到公共仓库,就不要对该分支进行rebase操作

想想如果通过rebase命令将master上代码合并到你的feature分支上。

设想出现上面的场景,rebase命令将master上所有提交记录移动到feature分支的改动之前。这个操作的问题在于只发生在本地。其他的开发者依然基于远程的master分支进行工作。因为rebase操作会生成新的commit,Git会认为你的master分支已经和其他人的主分支不一样了。

唯一的解决办法是将两个主分支merge成一个,这样会导致一条额外的commit记录,还有同一个改动会有两条commit信息(原来的commit和rebase后的commit),不用多说,这种情况是让人非常困扰的。

所以,在执行git rebase命令之前,问问自己:“是否有其他人也在用到这个分支?”,如果答案是“是”,那么请改用其他的方式。其他情况下,请随意使用rebase命令。

强制更新远程

如果试图将rebase后的master分支push到远程代码库,git会阻止你这么做,因为你的代码和远程的master分支是有冲突的。但是,你可以使用–force参数强制更新,像下面这样

git push --force

上面的操作会用你本地的master分支覆盖掉远程的master分支,这会让小组里的其他成员非常困惑。小心使用这个命令,除非你知道自己在做什么。

一种可以使用–force的情况是,你push了一个本地feature分支到了远程的代码仓库,之后整理了一下本地的提交记录后重新push到master分支。这就像在说:“我并不想push之前的feature分支,请无视它,并且使用现在的分支”。同样的,应该保证没有人在feature分支上提交了新的commit。

工作流

在开发中,应该根据实际情况来使用rebase。在这节里,我们会讨论在开发一个新feature开发的流程中,rebase操作能带来的好处。

在开发新的feature时,通常的做法是新建一个分支来专门进行开发。能够正常使用rebase命令的分支结构一般如下:

清理本地提交

对本地正在开发的feature使用rebase操作是最佳实践之一。时不时地执行交互式rebase,能够让分支上每一次commit记录更简洁。这会让你的开发流程中不用再担心commit的提交过于细碎--你可以在多次提交后将其整合成一次提交。
当使用git rebase时,可以有选择两个基准commit:1.feature分支的祖先分支,即本例中的master分支;2.feature分支之前的某次提交。在前面的例子里面我们使用过第一种情况。当你想整合最近的几次commit的时候,第二中情况是不错的选择。举个例子,下面的操作会整合最近的三次提交

git checkout feature
git rebase -i HEAD~3

HEAD~3指定为新的基准commit,就不会移动整个分支。在编辑完成后,只有最新的三次提交历史被重写了。注意这并不会将这个分支上upstream的改动合并到feature分支上。

如果想要整个重写feature的提交历史,git merge-base命令能帮助你找到feature分支的祖先分支。下面的命令将返回祖先分支的commit ID,你可以基于这个id来进行rebase

git merge-base feature master

在开发中使用交互式rebase是很不错实践,因为它只会影响到本地的分支。将修改过提交历史的分支发布到远程代码库,其他的开发者就能看到干净,简洁,易理解的提交历史了。

但是还是要强调,上面的操作只能对本地feature分支执行。如果在和其他的开发者一起在同一个分支上开发,那个分支就是公开的,你不能重写那个分支的提交历史。

使用git merge不能清理本地的提交记录

远程代码更新合并到本地分支

在之前的基础概念一节中,我们看到了一个feature分支使用git merge或者是git rebase将upstream的改动合并到本地。merge操作会保留整个历史记录,所以是安全的;rebase则会新建一个线性提交历史的分支。

git rebase的作用优点像执行本地清理,但是在执行过程中会将master分支上的upstream commit合并到本地分支。

要记住,基于远程非master分支进行rebase完全是可以的。在和其他人开发同一个feature时,需要将他们的提交合并到本地时,可以使用这一操作。

举例来说,现在你和一个叫John的开发者共同开发一个feature分支,在fetch了远程John提交的feature分支后,你的代码仓库如下图所示。

和之前的处理master分支的例子一样,你可以在本地feature分支上merge远程的分支,也可以rebase本地的分支,分别如下图

注意这样的rebase操作并没有违反黄金法则,因为只影响了本地feature分支的commit。这就好像在说:“在John改动的基础上进行提交。”在大多数情况下,这样子的提交记录比merge操作后的提交记录更简洁明了。

默认情况下,git pull命令会执行一次merge,但是可以通过–rebase参数将远程的分支rebase到本地。

Code Review

使用pull request(PR)进行code review时,不要在创建PR后使用git rebase。一旦提交了PR,其他的开发者就会审查你的commit,也就是说,这个分支公开了。重写这个分支的提交历史将会让git和其他的组员无法追踪后来加入的commit。

需要公开的改动都应该使用git merge,而不是git rebase。

出于这个考虑,你应该在提交PR之前使用rebase命令清理本地的代码。

合并通过review的分支

在一个分支已经被团队review通过后,你可以在该分支被merge到主分支之前将其rebase。

这有点像之前的feature分支合并主分支更新代码的情况。因为不能重写master分支的提交历史,所以只能使用git merge将feature分支合并到master上。但是在merge之前执行rebase,能让merge后的提交历史是线性的。这也给了你简化feature分支提交历史的机会。

如果对rebase用起来还不是那么顺手的话,你可以在临时分支上练练手,这样即使搞错了,也可以切回原来的分支,重新再来一遍。

git checkout feature
git checkout -b temporary-branch
git rebase -i master
# [Clean up the history]
git checkout master
git merge temporary-branch

结语

上面关于rebase就介绍完了。如果你想要一个干净,线性,没有多余merge信息的commit历史,应该使用git rebase,而不是git merge。
另一方面,如果想要完全保留工程的历史,并且避免重写公开commit的风险,也可以只使用git merge。这两个命令在合并代码的时候都很好用,但是现在你能根据实际情况作出更好的选择了。

如果喜欢这篇文章的话,不妨点个赞加关注呗~