原文:Merging vs. Rebasing
git rebase
一直是初级 程序员 想远离的黑魔法,但事实上在开发团队小心的使用该命令可以令我们的工作更轻松。本文将git rebase
与git merge
做对比,来看看在典型的Git工作流中引入git rebase
可以带来什么好处。
概念纵览
首先我们要了解git rebase
和git merge
作用是相同的,都可以用来将一个分支上的代码合并到另一个分支——只是实现方式不同。
当你使用专门的分支开发feature时,其他团队成员如果继续向master分支提交。就会产生一个分叉的历史,任何使用Git作为协同 工具 的开发人员应该都很熟悉这样的场景。
假设提交到master的代码跟你正在开发的feature相关,为了将新的提交合并到feature分支,你有两个选项:
git merge
或git rebase
。
Git merge
最简单的方法就是使用以下命令将master分支合并到feature分支:
git checkout feature
git merge master
或者简化成一行命令:
git merge master feature
该操作会在feature分支上产生一个新的“合并提交”,将两个分支的历史合并在一起,如图示这样:
git merge
很好用因为它是一个非破坏性的操作。当前分支不会被改变。因此避免了git rebase
操作可能带来的所有隐患(详见下文)。然而另一方面,这意味着每一次合并操作都会在feature分支上产生一个额外的“合并提交”。如果master分支更新频繁的话会导致feature分支的历史记录被污染,令其他开发者难以理解项目的历史记录。
Git rebase
合并操作的另一个选择是使用git rebase
,例如如下命令,将feature分支rebase到master分支:
git checkout feature
git rebase master
该操作将整个feature分支的记录移到了master分支的头部,将所有的新提交都并入了master分支。但是不同于"合并提交",git rebase
通过对原分支上的提交做标记重写了项目的提交记录。
使用
git rebase
的最大好处是可以得到更干净的项目记录。首先,它消除了git merge
带来的无用的"合并提交"。其次,对比上面的图我们看到,git rebase
可以产生完美的线性历史记录——可以方便的使用git log
,git bisect
和gitk
追踪提交记录。但是,在追求干净的提交记录的同时还要权衡两个方面:安全性和可追溯性。如果没有遵循
git rebase
的最佳实践,重写项目的历史记录可能会毁灭合作工作流。另外,git rebase
丢失了合并操作的上下文信息——我们无法得知某一次合并操作具体发生在何时。
交互式rebase
交互式的rebase可以对要移到新分支上的提交记录进行修改,它能够完全控制分支的提交记录,因此比自动rebase功能更强大。通常,在将一个功能分支合并到master分支前,可以用其来整理混乱的提交历史。
将-i
传入git rebase
来进入交互式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。最终项目的提交记录如图所示:
通过交互式rebase可以忽略不重要的提交令分支历史更容易理解,这一点是
git merge
无法比拟的。
Git rebase的金律
现在我们理解了什么是git rebase
,但最重要的是要知道什么时候不要用它。Git rebase的金律是:永远不要在公有分支上用它。
例如,试想一下如果将master分支rebase到feature分支会发生什么:
这个rebase操作将master分支的所有提交移动到了feature分支的头部。但问题是这一切都发生在你本地。其他开发者依然在原master分支上继续工作。因为rebase会导致新的提交,Git会认为你的master分支和其他开发者的出现了分叉。
唯一同步这两个版本master的方式是将它们
git merge
,但是这样会导致一个额外的合并提交记录和包含相同修改的两次提交(原始master分支和被rebase的master分支)。不用说,这是一个令人困惑的情况。所以,在执行
git rebase
之前,总是问问自己,“其他人用这个分支吗?”如果答案是yes,把手从键盘上拿开,考虑使用一种非破坏性的方法来达到同样的目的(例如git revert
)。否则,放心的按自己的喜好去重写历史吧。(译注:修改历史提交记录)
Force-Pushing
Rebase过的master分支与远程仓库的master分支会存在冲突,所以Git不允许将rebase过的分支推送到远程仓库。但是可以通过传递--force
标签进行强制推送:
# Be very careful with this command!
git push --force
该操作会将远程仓库的master分支替换为rebase过的master分支,这会给团队的其他成员带来困扰。所以,小心使用该命令,除非你知道自己在做什么。
少数几个需要使用强制推送的场景之一是将私有分支推送到远程仓库(例如:以备份为目的)后,对该分支上的提交进行了清理。这就像说,“我不想推之前那版feature分支了,用这个替换吧”。再强调一次,一定要确保没有其他开发人员在使用该分支。
工作流演练
git rebase
可以根据团队的需要或多或少的与现存的Git工作流进行整合。这一章,我们来看看在一个feature分支的不同开发阶段,使用git rebase
能带来什么好处。
对于任何工作流来说,要使用git rebase
的第一步是为每个feature创建一个专门的分支。这种分支策略可以帮助我们安全的使用git rebase
:
整理本地分支
将rebase
加入到工作流的一个好处是整理本地进行中的功能分支。通过定期执行交互式rebase,可以保证每一次提交(commit)都是有意义的。这可以让我们在写代码时不用担心每次提交是不是都有意义——可以之后通过交互式rebase修复。
调用git rebase
时可以传入两种参数:父分支(例如master),或当前分支的前几次提交。在交互式rebase章节中我们看到了传入父分支的用法。当我们需要整理最后几次提交时后者比较有用。例如,以下命令为最后3次提交开启了交互式rebase。
git checkout feature
git rebase -i HEAD~3
通过指定HEAD~3
,我们并没有移动分支——只是重写了最后三次提交。注意不包含其他提交。
如果想用这个方法重写整个分支,
git merge-base
命令可以用来追寻feature分支的original base。以下命令返回original base的commitID,可以将该commitID传给git rebase
:
git merge-base feature master
可以通过这种交互式rebase的用法将git rebase
引入到你日常的工作流中,因为它只会影响本地分支。其他开发者仅仅会在你开发完成之后看到一个提交记录干净的,易于追溯的feature分支。
但是再一次强调,这种用法只针对本地私有分支。如果你在和其他开发者在某一个feature分支上进行合作开发,那么这个分支是共有的,请不要试图去重新提交历史。
git merge
无法做到清理本地提交历史。