git

Git 系列2/3:Rebase和其黄金法则的解释

Posted by jygao on April 12, 2020

在git rebase期间到底发生了什么,为什么你需要关心这个呢?

本文是我们在上一篇探索.git 目录的下篇。

rebse的基础

当你想到git rebase时,你脑子中可能会出现下图:

当你做rebase操作时,你可以这么说:从你想要rebase的分支上拔掉,然后再插到另外一个分支的顶端。这样说很接近真相了但我们可以再深挖一点。如果你看官方文档,关于rebase它是这么写的:把当地的commits转接到更新的上游的顶部。

是不是好像并没有什么帮助?一个合理的翻译可以是这样:git rebase,从你的分支上把所有的commit重新作用一遍到另外一条分支的顶部。

最重要的词是“重新作用(reapply)”,因为rebase并不是从一条分支到另一条分支的简单的复制粘贴。一次rebase会按顺序的从你当前的分支上拿出所有的提交,并把它们重新作用到目标的分支上。这个行为主要有两个影响:

  1. 对commits的重新作用会产生新的commits。这些新的commit,即使它们更改的内容一样,但是会被git当做完全不同和独立的commit来对待。
  2. Git rebase会重新作用commits,这并不会销毁那些旧的commits。这意味着,即使在rebase之后,你的旧的commits仍旧在.git 目录的objects的文件夹下。如果你对git 是如何理解和存储commit并不是很了解的话,你可以点击这里学习更多内容。

因此以下可以更准确的说明了,在rabase过程中真正发生了什么:


正如你所看到的,feature分支上有了全新的commits。之前说过的,同样的更改的内容,但是从git的角度来说是完全不同的对象(objects)。然后你也能看到之前的commits并没有并销毁。它们只是没有那么直接可以访问到。如果你记得,一个分支只是一个指向commit的指针。因此,如果没有任何指向commit的分支或者tag,那几乎不可能访问到commit,但是它们仍旧在那里。

下面让我们谈一下著名的黄金法则:

rebase的黄金法则

“谁也不可rebase一个共享的分支” — 每个人都知道rebase的要点

你可能碰到过这个法则,也许是以一种别的说法。即使你没有听过,这个法则也是很简单的。从不,“从不”,rebase一条共享(shared)的分支。所谓的共享的分支指的是已经存在远程存库里面,其他人可以拉取的分支。

这条法则经常被称作黄金法则。我认为理解它是一件好事,如果你想增强对git的认识。

为了理解,让我们假设有一位开发人员不遵守了该法则,然后看看发生了什么:

让我们假设Bob和Anna在同一项目中工作。这里是Bob,Anna本地和仓库和在GitHub上的远程仓库:


Everybody is sync with the remote (GitHub)

现在Bob,非常无辜地,违反了这个rebase的黄金法则,在此期间,Anna决定在feature分支上工作并创建了一个新的commit:


Do you see what’s coming ?
E

你看出接下来将要发生什么了吗?

Bob现在打算push,他被拒绝了并且收到以下提示:

以上用了Oh My Zsh 的无知主题,需要了解可以点击了解。

这里git不开心了,因为它并不知道怎么合并Bob的feature分支和Github上的feature分支。当你push你的分支到远程仓库,git将你要推送的分支和远程上当前的分支合并。事实上,git会尝试快进(fast-forward)你的分支,我们以后的文章会讨论到。现在你需要记住的是,远程仓库并不能,以如此简单的方式,处理Bob要推送的rebase过的分支。

对于Bob,现在一种解决方法是:git push --force,它无非这样告诉远程仓库:不要尝试把我要推送的分支和你的现有的分支合并,擦掉现在你的feture分支的版本,我现要推送的是一个新的feature分支。所以,我们最后会得到如下:

Had Anna known what’s coming, she wouldn’t have come to work this morning.

H如果Anna知道接下来会发生什么,她今早可能不来上班了。

现在Anna想要推送她的更改:

以上结果并不奇怪,git只是告诉Anna她还没有同步最新的feature分支,也就是说,她的分支的版本和GitHub上的版本不一样。因此,很自然地,Anna 做了拉取操作(pull)。当你推送时,git尝试把你本地的分支和远程仓库上内容合并,跟你拉取时,它把远程仓库内容和你本地分支合并,是一样的道理。

你拉取前,远程和本地的分支长这样:

A--B--C--D'   origin/feature // GitHub 
A--B--D--E feature // Anna

当你拉取时,git 不得不未解决这个问题要去做一个合并。然后发生了如下:

这个commit M代表的是merge的commit,Anna和GitHub的特性分支最后被联结的地方。最后Anna处理完所有的合并冲突,推送她的更改,最后终于可以松一口气。Bob决定拉取最新,然后所有人都已同步最新了。

这样混乱的局面已经足够让你相信这个黄金法则的有效性了。你所知道的是,在你眼前的混乱局面是被一个人制造出来的,在一条被两人共享的分支上。想象一下一个10人的团队。众多人们使用git的原因之一是,git可以让你轻易回到过去,但要注意的是,你的历史越混乱,就越复杂。

你可能也注意到:在远程分支上D 和D'两个提交其实有着一样的更改。基本上,重复的commit的数量跟你rebased 的分支上commit的数量一样多。

如果你还不大信服,试想一下,Emma,第三位开发,她工作在Bob搞砸所有事前的feature分支上,现在她想要推送她的更改。注意她是在我们上一个场景后要推送。

该死的Bob!


更新:有些读者提到,本文可能让你认为rebase只能基于一条分支到另外一条分支的顶部。事情并非如此,你可以基于同一条分支,但这又是另外一个话题了。