这并不是真正的“邪恶”,而是实现和期望的问题。
我们从一堆事实开始:
每个 Git 哈希表示某个唯一的对象。针对我们在这里需要考虑的只有提交对象。每个哈希是将加密哈希函数(对于 Git 具体来说,是 SHA-1)应用于对象的内容的结果。对于一个提交,其内容包括源树的 ID;作者、提交者的姓名、电子邮件地址和时间/日期戳;提交消息;以及最重要的,父提交的 ID。
即使只更改内容中的单个位,也会得到一个新的、非常不同的哈希 ID。哈希函数的加密属性,它们用于验证每个提交(或其他对象),还意味着没有办法让一些不同的对象具有相同的哈希 ID。Git 也依靠此功能在仓库之间传输对象。
变基通过将提交复制到新提交来工作(必须)。即使除此之外没有任何变化——通常,与新副本相关联的源代码与原始源代码有所不同——变基的整个重点是重新定位一些提交链。例如,我们可能从以下内容开始:
...--o--*--o--o--o <-- develop
\
o--o <-- feature
分支feature
在提交*
时与分支develop
分离,但现在我们想要feature
从develop
的最新提交处开始,所以我们将其变基。结果是:
...--o--*--o--o--o <-- develop
\ \
\ @--@ <-- feature
\
o--o abandoned [used to be feature, now left-overs]
两个@
是原始两个提交的副本。
分支名称(如develop
)只是指向(单个)提交的指针。我们认为“分支”的东西,如两个提交@--@
,是通过从每个提交向其父级倒退形成的。
分支总是期望增加新的提交。很正常发现develop
或master
新增了一些提交,因此该名称现在指向一个提交,或者是指向许多提交中的最后一个提交,而这些提交都指回了该名称所指的位置。
每当您让您的Git与其他Git及其另一个存储库同步(无论到什么程度),您的Git和他们的Git都会交换ID -- 具体取决于传输方向以及您要求您的Git使用的任何分支名称。
远程跟踪分支实际上是与您的存储库相关联的实体。您的远程跟踪分支origin/master
实际上是您的Git记住“在我们上次通话时,origin
的Git说他的master
是什么”。
现在,我们将这七个项目作为示例,看看git fetch
如何工作。例如,您可以运行git fetch origin
。此时,您的Git调用origin
上的Git,并询问其有关其分支的信息。它们会说类似于master = 1234567
和branch = 89abcde
的内容(虽然哈希值确实都是40个字符长,而不是这些7个字符长)。
如果您的Git已经拥有这些提交对象,则几乎完成了!否则,它会请求他们的Git发送这些提交对象,以及您的Git需要理解它们的任何其他对象。附加的对象是与这些提交一起的任何文件,以及那些提交使用但您还没有的任何父提交,以及父提交的父提交,依此类推,直到找到您拥有的某些提交对象。这将为您提供所有新历史记录所需的所有提交和文件。1
一旦您的Git安全地存储了所有对象,它将使用新的ID更新远程跟踪分支。他们的Git刚刚告诉您,他们的master
是1234567
,因此现在您的origin/master
设置为1234567
。对于他们的branch
也是一样:它变成了您的origin/branch
,而您的Git保存了哈希89abcde
。
如果现在您运行git checkout branch
,您的Git将使用origin/branch
创建一个新的本地标签,指向89abcde
。让我们来画一画:
...--o--*--o--1 <-- master, origin/master
\
o--8 <-- branch, origin/branch
为了使事情变得更有趣,让我们在分支branch
上进行自己的新提交。假设它被编号为aaaaaaa...
:
...--o--*--o--1 <-- master, origin/master
\
o--8 <-- origin/branch
\
A <-- branch
那么有趣的问题是,如果它们(你获取的Git)重新设置基础,则会发生什么。假设他们将 branch
重新设置到 master
上。这会复制一些提交记录。现在你运行 git fetch
,你的 Git 看到他们说 branch = fedcba9
。你的 Git 检查是否有此对象;如果没有,则获取它(和其文件)及其父代(和该提交的文件),直到我们达到某个共同点 - 实际上将会是提交记录 1234567
。
现在,你拥有了这个:
...--o--*--o--1 <-- master, origin/master
\ \
\ o--F <-- origin/branch
\
o--8--A <-- branch
我这里用F
来表示提交fedcba9
,也就是现在origin/branch
指向的那个提交。
如果你后来看到这个提交历史时没有意识到上游开发者已经变基了他们的branch
(你的origin/branch
),你可能会认为你写了所有三个提交o--8--A
,因为它们在你的branch
而不再在origin/branch
上。但它们不在origin/branch
上的原因是上游开发者已经放弃了它们,取而代之的是新的提交。这有点难以确定那些新的提交实际上是副本,并且你也应该放弃这些提交。
1如果分支按着“正常”的、“预期”的方式增长,你的Git和他们的Git很容易找出你的Git需要从他们那里获取哪些提交:你的origin/master
告诉你上次看到他们的master
在哪里,现在他们的master
指向更长的提交链的末端。你需要的提交恰好是在他们的master
上,且出现在你的origin/master
末端之后。
如果分支以不太典型的方式洗牌,则有些困难。在最一般的情况下,他们只需按哈希ID枚举所有对象,直到你的Git告诉他们他们已经到达一个你已经拥有的提交为止。具体细节还受浅克隆的影响。
这并非不可能
这并非不可能,自Git 2.0版本以来,现在内置了工具可以让Git帮你找出来。(具体来说是通过git merge-base --fork-point
调用,由git rebase --fork-point
使用,它使用你的origin/branch
的引用日志(reflog)来确定o--8
链曾经在origin/branch
上。这仅适用于那些引用日志条目被保留的时间段,但这默认至少为30天,让您有一个月的时间去跟进。这是你的时间线中的30天:从你运行git fetch
开始算起,而不是上游变基发生多久之前。)
这实际上归结为,如果你和上游开发者事先同意某个特定的分支集合被变基了,那么你可以安排在他们每次进行变基时在你的代码库中做出必要的调整。但是,在更典型的开发过程中,你不会期望他们进行变基,如果他们没有这样做——即他们从未“放弃”你获取到的一个已经发布的提交——那么就没有什么需要从中恢复的东西。
git fetch
时,你从别人那里收集一些名称/ID对,并且你的 Git 重命名这些名称,并将ID作为“远程分支”添加/更新到你的存储库中。一旦fetch
完成,这就没有什么remote了:它只是你的Git记住“上次我和他们交谈时,他们说master
是1234567...
”。 - torek