接上文 Git: commit 中的 hash 是什么
在上文中,我们讨论了commit hash
是什么。我们了解到的关于commit
的一个重要方面是它们不能被改变。hash
本身是由存储在commit
中的信息生成的,因此要修改commit
或commit hash
,必须创建一个全新的commit
。我们还讨论了每个commit
都存储上一个commit
的hash
。我们没有讨论的是这对我们的Git历史有什么影响。
由于commit hash
是根据存储的信息生成的,并且部分信息是前一次commit
的hash
,因此几乎不可能修改提交历史记录。每一个提交就像是一个链条中的一个链环,它是依据前一个链环锻造的。
如果你像上面那样从金属链上拆下一个链环,就不可能在不破坏它们的情况下重新连接上一个链环和下一个链环。然而,在Git的上下文中,情况更糟;这个类比在这里被打破了,因为在一个金属链中,你可以建立一个新的链环,将前一个链环和下一个链环连接在一起。在Git中不能这样做;在这里可以重新锻造的唯一链环必须是完全相同的链环,包含完全相同的信息,包括以前的提交哈希。只有精确的提交才能具有链中的下一个链环所引用的commit hash
。
如果要在提交历史记录中间删除一个提交,那么下一个提交将引用一个不再存在的commit hash
。由于在不更改commit的hash的情况下无法更改该提交,因此不能简单地生成引用上一次提交的新提交,因为链中的下一次提交引用了原始提交的hash
。
如果您更改了一个提交的一小部分,那么为它生成的hash
将是不同的,并且链中的下一个提交将不再引用新的提交。您必须更改下一个提交以引用新生成的commit hash
,这也会导致该提交的哈希发生更改。一直到链子的末端。
这就是rebase
发挥作用的地方。如果您还记得第2部分中的内容,在我们将feature1分支合并为master
之后,在我们的图表中留下了一个叉,显示了我们所有的提交是如何与彼此相关的。
合并可以很好地工作,但是存储库图可以很快失去对所有分支和交叉提交关系的控制。下面只是我的一个仓库的一个小片段。
如果您使用Git的图形界面,那么很有可能看到类似的东西。合并是在分支之间移动更改的最简单方法,因为它们避免破坏提交历史链,并减少了令人头疼的问题。然而,一旦你对rebase
的工作有了一个强有力的理解,你就开始对它有所欣赏。例如,如果我们将演示存储库中的feature1
分支以master
分支为基rebase
,那么我们将得到一个干净的历史记录,如下所示:
请注意,我们的历史又是一条简单的直线。Git做了什么魔术才能让这成为可能?如果您还记得之前的内容,我们的commit 3和commit 4将共享commit 2作为一个共同的祖先提交。提交3引用了提交2,因为它是前一次提交。现在您可能想知道为什么commit 3
把commit 4
列为它的previous commit
。
还记得我说过的话吗?如果你在中间断了锁链,那么你就必须从头到尾重建所有的commit。这正是rebase
所做的。
如果你仔细观察,会发现commmit 3
、commit 5
和commit 6
的commit hash
都已更改。这是对我们的feature1
分支作出的三项commit
。通过将我们的feature1
分支rebase
到master
上,Git能够将我们的分支倒回到它最初与master分离的地方。它将分支上每个提交的diff存储在一个临时文件中。然后它开始重写我们的分支历史,但这次是从master
上最新的commit
,也就是commit 4
开始。
Git为我们分支上的每个提交创建了全新的提交,并彻底使用全新的commit hash
。当它创建新的commit
时,它会更改我们分支上的第一个提交,以便它现在引用master上的最新提交作为previous commit
。将更改重新提交为新提交的过程称为以master为基础重新commit。
注意:不要被术语混淆。以
master
变基 不会修改master
。这意味着你的分支提交将在master
的最后一个commit
之后立即执行。
在上面的屏幕截图中,您会注意到master
指针仍然指向commit 4
,其commit hash
没有更改。如果我们现在切到master
并将feature1
合并到master
,我们将无法获得合并提交。这将是一个简单的快进合并,这意味着Git只需将主分支指针向上移动我们现在的直线提交引用,以便它指向与feature1
分支指针相同的commit
。
如果不将feature1
合并到master
中,我们决定做更多的工作并进行更多的提交,我们将在图中再次创建一个分叉。我们的下一个提交给master
将引用commit 4
作为它的祖先,而我们的feature1
分支的第一个提交也将引用commit 4
作为它的祖先。为了再次得到一条直线,我们需要检查feature1
,并再次rebase
到master
上。如果您曾经通过Github提交了一个pull request
并过时的时候,那么基本上就是这样。如果项目维护人员没有将拉请求合并进来,而是继续在项目上做更多的工作,那么这个pull request
需要rebase
来包含完整的的Git历史记录。将您的工作rebase
到repository/branch
上使得 Pull request
简单地将其分支指针快速转移到分支接受后指向的同一提交。接受 Pull request
只是一个简单的合并。如果您在提交请求之前进行了rebase
,并且在提交更多请求之前将其合并,那么合并将是一个 fast-forward
的合并,并将保持原始存储库的整洁。
DANGER, DANGER WILL ROBINSON!
到目前为止,我已经向您展示了如何将一个功能分支以master
为基准rebase
,而master
不会修改已经在master
上的任何提交。rebase
只会修改功能分支的所有提交。但是,如果你试图通过以功能分支为基来变基master
,那么你就是在自找苦吃。
假设你的主分支要通过克隆仓库的方式与所有人共享。如果你在master或任何其他共享分支上push
一堆提交,然后我将这些提交pull
到我的机器上,那么现在我可以继续执行我的工作,并在你的提交之上提交新的更改。我的第一次提交现在将引用你的最后一次提交作为它的previous commit
。
假设您对本地master
分支进行了五次提交,因为feature1
分支与之不同,有三次提交。我们还将假装您将master
上的提交推送到了remote repository
(远程仓库)中。
现在你做了一个错误的操作,以feature1
为基本变基master
而不是以master
为基本变基feature1
,现在你有了一个整洁的历史。但是,你会注意到,origin/master
似乎位于一个奇怪的分叉上。
如果你现在试图把你的主分支push
上去,Git会拒绝。Git认为你的远程master
上有五个个commit
需要你pull
。它还认为有八个commit
需要push
。
如果你试图在这种情况下pull
,事情会很快令你发毛。你拉下来的五个commit
的是旧的commit
,它们的的commit hash
和你rebase
之前的commit hash
是一样的。
讨厌!(Yuck!)
"Aha!" you say, "I’ll simply git push --force and all will be well!"
DON’T YOU DARE!
通过强制推送,您只是将这个问题丢给了别人,因为别人正在基于你的旧的commit hash
工作。当前,我对您的存储库的克隆如下所示。
--force
将告诉远程存储库只需丢弃那些旧的提交,并使用你的提交。现在,下次我pull你的仓库的时候,我会处理你所造成的混乱。
我现在有奇怪的合并冲突和奇怪的重复提交。我不知道我应该在哪里继续我的工作,也不知道我的工作将如何与你的工作同步。
这就是人们需要谨慎使用rebase
的原因,同样也需要谨慎使用git push --force
。解开一根钢筋并不容易,而且常常是不可能的,所以你真的需要注意你在做什么。rebase
的好处是很好的,除非你不知道自己在做什么的。如果不小心,可能会导致大量工作丢失,因为您必须将仓库回退到没有rebase
时的提交,并且无法从其他地方恢复它们。
在我们设想的糟糕场景中,修复方法是将存储库reset
为feature1
分支上的最后一次提交。然后,您需要从远程存储库中进行一次pull
,以恢复您在rebase
之前推到那里的五个主提交。然后,您可以切到feature1
分支,并正确地执行REBASE。
我们假设的情况很容易恢复,但是如果你失去冷静,开始做事情而不知道你在做什么,那么你很容易永远失去这五项commit
。如果你git push --force
并丢弃你的远程存储库中最初的五个提交,那么在你看来,一切都会像在前一个屏幕截图中一样漂亮。也就是说,直到你的项目参与者开始在issue
或消息列表中诅咒你。这五个旧的commit
将是敬酒,你恢复它们的唯一方法,将是找一找,谁之前pull
过这五个原始commit
,并从他那里恢复。
底线:除非您知道没有其他人在远程存储库或分支上工作,否则不要强制推送。在我工作的地方,每个开发人员都有自己的远程仓库的fork
,我们将提交包含我们自己修改的pull request
。在我已经有一个挂起的pull request
之后,解决一个小bug是很常见的。
我经常会修复这个bug,并使用commmit--amend
修改最后一个与git commit
,这相当于rebase
最近的commit
,并给它一个新的commit hash
。我没有问题强制将更改推到我的fork,并用旧的hash
清除旧的commit
,因为我知道我是唯一一个处理该fork的人,我的pull request
尚未合并。如果它已经被合并了,那么我就不能再将这个经过修改的commit
推到我们的主仓库了,因为原来的提交已经被合并了,而其他人可能会将其拉下来。如果我在这一点上push --force
并打开一个新的pull request
,我可能会给我的团队中的其他人带来问题。
小心翼翼rebase
。一旦你掌握了它的窍门,你就不会再掉进这样的陷阱了。小心点,注意你在做什么。rebase
的障碍起初几乎总是很小的障碍;当用户试图“修复”它并且不知道他们在做什么时,它们只会变成厄运的黑洞。
记住,将一个分支rebase
到另一个分支上的简单方法是:断开当前分支分叉的链环,并开始将全新的链环锻造到另一个分支的末端,以便每个链环接仅连接到一个先前的链环,而没有两个链环连接到同一个先前的链环。