原文
目录
git rebase
命令以魔法巫毒著称,初学者应该远离它,但它实际上可以使开发团队在使用git时更加轻松。在本文中,我们将比较git rebase
和相关的git Merge
命令,并说明将rebase
合并到典型的Git工作流中的所有场景。
首先我们要知道git rebase
和git merge
解决的是同一个问题,二者都是将某个分支的更改应用到引入到另一个分支,只是使用的方式不同。
考虑这样一种场景:你在一个新建分支上开始自己的工作,与此同时,其他人在master
分支上更新了新的提交。这导致了分叉式的历史,任何使用Git作为协作工具的人都应该熟悉这种场景。
现在,假设master
中的新提交与你当前分支相关,要将新提交合并到你当前分支中,你有两个选择:merge
或rebase
。
最简单的方式是将master
分支合并到feature
分支,如下:
git checkout feature
git merge master
或者将其简写成一行:
git merge feature master
这种方式会在feature
分支创建一个新的merge commit
,将两个分支关联在一起,现在分支结构如下图:
merge操作不具有破坏性,已有分支不会被改变。这避免了rebase有可能导致的所有问题(稍候讨论)。
另一方面,每次合并时,feature
分支都会产生一个额外的合并提交。如果master
分支非常活跃,这会对你的分支整洁度造成相当大的影响。虽然可以使用高级的gitlog
选项来缓解这个问题,但它会使其他开发人员很难理解该项目的历史。
作为合并的另一种选择,您可以使用以下命令将feature
分支重新rebase
(变基)到master
分支:
git checkout feature
git rebase master
这将使整个feature
分支从master
分支的尖端开始,有效地将所有新提交都合并到master中。不同于merge产生一个merge commit
,rebase
根据原commit
创建新的commit
来重写项目历史。
rebase的主要好处是你获得一个干净的历史。首先,它消除了git merge
所需的不必要的合并提交。第二,正如您在上面的图表中所看到的,rebase
还会生成一个完美的线性项目历史,更便于查看历史。
但对此有两点需要考虑:安全和可追溯性。如果您不遵循rebase
的Golden Rule of Rebasing,重写项目历史可能会对您的协作工作流程造成潜在的灾难。而且,更重要的是,重基会丢失合并提交提供的上下文–你无法看到何时将上游更改合并到特性分支中。
当启用交互式rebase时,你可以在创建新commit时修改他们。这点很有用, 因为我们可以更自由的控制分支的提交历史。通常,这用于在将特性分支合并到主分支之前清理自己混乱的历史记录。
传入-i
参数启用交互式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
这个清单清楚展示了在执行reabse
之后分支的样子。通过改变pick
命令或各行排列顺序,可以依你所想来定制分支的提交历史。例如,如果第二个commit是对第一个的小小修复,你可以通过fixup
命令将他们合并成一个commit:
pick 33d5b7a Message for commit #1
fixup 9480b3d Message for commit #2
pick 5c67e61 Message for commit #3
保存并关闭文件,Git会分根据文件内容执行rebase
,新的项目历史如下:
清除不必要的提交可使分支历史更清晰易懂,这是git merge
很难做到的。
一旦你明白了什么是rebase
,最需要学会的是知道什么时候不能使用它。使用reabse
的原则是:不要在公共分支上使用。
例如,想一下如果把master
分支reabse
到你的feature
分支上会发生什么:
这个操作会把master
分支是的commit
移动到feature
分支上,此时的问题是这些变化只发生在你本地,其他协同开发者仍然以远程master为参照。由于rebase
生成了新的commit
,Git会认为你的master
分支历史和其他人的有了分叉。
同步两个master
分支的唯一办法是将它们重新合并,结果就是,会产生一个额外的commit,且包含两套内容相同的commit(原始的一组,rebaser后创建的一组)。不用说,这是一个非常混乱的局面。
所以,在你运行rebase
前,要先想下,“是否还有其他人使用这个分支?”,如果有,那就住手,另想他法吧(如git revert
),否则,你可以随意重写历史。
如果你尝试推送变基过的master分支到远程,git会阻止你,因为本地分支和远程分支有冲突。但是,你可以使用--force
选项强制推送到远程,如下:
# Be very careful with this command!
git push --force
本地分支会覆盖远程原分支,会让组内其他人感到困惑。所以,确保你知道会发生什么,不然不要轻易使用该命令。
只有当你在本地做了一些清理,且推送到一个私人的远程分支时,才允许使用该命令。类似这种情况:”糟糕,我并不想推送原先那个版本的,用当前版本替代他吧“。再次强调,要确认没有其他人使用当前远程分支。
可以将变基合并到您现有的git工作流中,或多或少地帮助你的团队。我们将看一看在特性开发的各个阶段rebase
所能带来的好处。
任何利用git rebase
的工作流的第一步是为每个特性创建一个专用分支。这为您提供了安全使用变基的必要分支结构:
整合到工作流中的最佳方式之一是清理本地的、正在进行的功能。通过定期执行交互式变基,你可以确保特性中的每个提交都是有重点的和有意义的。这使你在编写代码时,不必担心将其分解为孤立的提交–你可以在事后重新整理。
执行git rebase
时,确定重新开始的基础有两种方式:一个是使用特性分支的父分支(比如master),一个是当前分支的较早的commit。第一种我们之前已经讨论过,第二种用来处理只需要修复最近的几次提交的情况再好不过了。如下命令以倒数第四个commit为基点处理最后三个commit:
git checkout feature
git rebase -i HEAD~3
通过指定HEAD~3
作为新基点,实际上,你并没有移动分支–只是在交互地重新编写之后的3次提交。请注意,这不会将上游更改合并到特性分支中。
如果你想用这种方法来重写整个分支,git merge-base
命令可以帮你找出当前分支的基点。如下命令返回原基点的commit id
,然后可以将其传给git rebase
:
git merge-rebase feature master
这种交互式重基的使用是将git重基引入工作流的好方法,因为它只影响本地分支。其他开发人员只会看到一个干净且易于跟踪的最终版分支历史。
但再次地,一定要确保是在私人分支进行操作。如果你通过同一个特性分支与其他开发人员合作,则该分支是公开的,并且不允许重写它的历史记录。
git merge
没有类似功能。
之前我们展示了使用git merge
或git rebase
将master分支的更改合并到特性分支中,merge
是一个安全的方式,它保存了存储库的整个历史,而rebase
通过将功能分支移动到master的末端,创建了一个线性历史记录。
这种git rebase
的使用类似于本地清理(并且可以同时执行),但是在这个过程中它合并了来自主分支的上游提交。
请记住,变基到远程分支而非主分支是完全合法的。这可能发生在与其他开发人员就同一特性分支进行协作时,并且你需要将他人的更改合并到你的存储库中。
例如,如果你和另一个名为John的的开发人员添加提交到特性分支,那么在从John的存储库中获取远程特性分支后,您的存储库可能如下所示:
你可以像引入master分支上游变化一样解决这个问题:合并local feature
和john/feature
,或在john/feature
的基础上rebase你的local feature
。
请注意,此重基并不违反重基的黄金规则,因为只有改变了你的本地特性提交–在此之前的所有内容都是未被更改的。这就像在说,“把我的改变加到john已经做过的事上。”在大多数情况下,这比通过合并提交与远程分支同步更直观。
默认情况下,git pull
命令执行merge
,但通过传递--rebase
选项,可以强制它将远程分支进行rebase
操作。
如果你使用Pull Request作为代码审查的一部分,在创建Pull Request后,就要避免使用git rebaes
。一旦你发出Pull Request,其他开发者看到了你的提交,你的分支就变成了公开的,重写分支历史会让其他人很难合并你的提交。
来自其他开发者的任何改变都只能通过git merge
而非git rebase
合并进来。
因此,在提交Pull Request前使用git reabse
清理自己的代码是很好的做法。
在被团队认可后,你可以使用rebase
将特性分支变基到主分支,然后再使用git merge
合并到主分支。
这和合并上游变化到特性分支类似,但是不允许重写主分支的历史,你最终还是需要使用git merge
合并特性分支。但通过先执行rebase
,可以保证merge
将fast-forward
,仍然会产生一个线性提交历史。这给你在pull request期间仍然能压缩整理提交的机会。
如果你对git 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
全部所需要知道的。如果你倾向于生成一个干净、没有不必要的合并提交、线性的历史,你应该使用git rebase
。
相反,如果你更想保存完整的项目历史,不愿冒重写公共提交的风险,你可以坚持使用git merge
。两个选择都是完全没问题的,但至少你现在有了从git rebase
得益的选择。