Git 因"Unrelated Histories"而拒绝拉取,远程和本地存储库都包含完全相同的文件和更改



几个小时以来,我一直在寻找有关此问题的更多信息。我正在清理存储在PC上的一些较旧的git存储库,并确保它们已完全提交并推送到上游的GitLab,然后再删除本地副本。当我尝试拉动时,大多数存储库都会给出错误"致命:拒绝合并不相关的历史记录"。在Google上搜索此错误会出现一些StackOverflow帖子,建议我使用"--allow-nonrelated-history"来解决问题,但这并不能帮助我理解为什么它首先发生。

我从 GitLab 克隆了一个较小的存储库,并对所有工作文件进行逐个文件比较。它们是相同的。我对另一个小回购做同样的事情并得到相同的结果。我决定检查日志。本地副本和克隆副本包含完全相同的提交集,本地副本具有空暂存区域。

这时我注意到本地存储库和克隆存储库对每个提交都有不同的作者信息。考虑到其他一切都是一样的,包括提交时间到秒,我只能假设这就是问题所在。我不明白为什么存储库的本地和上游副本具有不同的作者信息。据我所知,我还没有积极重写我的本地历史,而 GitLab 这样做本身似乎具有破坏性。

tl;dr:Git 拒绝合并不相关的历史。研究和比较本地和远程存储库,两者都是相同的,但奇怪的例外是本地和远程之间的每次提交时作者信息都不同。不知道为什么或正确的修复是什么。

TL;博士

考虑到您正在做的事情 - 合并有人将所有提交重新复制到新哈希 ID 的存储库 - 这是正常的。 它本质上也是无法恢复的,这就是为什么使用git filter-branch重写所有历史有点问题的原因。

"不相关的历史记录"的意思就是:有两个历史记录 - Git 提交图中的两个提交集合 - 彼此不链接。 关键是了解 Git 提交图的工作原理。

Git 存储库中的历史记录是(是?)提交。 每个提交都有一个哈希 ID;从非常真实的意义上讲,这是提交的"真实名称"。 提交本身的实际内容相当小。 以下是 Git 存储库中针对 Git 本身的提交:

$ git cat-file -p HEAD | sed 's/@/ /'
tree 4ec41fbdfd4e9569fceb3e25d4c1945f1944af0e
parent e66e8f9be8af90671b85a758b62610bd1162de2d
author Junio C Hamano <gitster pobox.com> 1528116101 +0900
committer Junio C Hamano <gitster pobox.com> 1528116101 +0900
Git 2.18-rc1
Signed-off-by: Junio C Hamano <gitster pobox.com>

此提交的哈希 ID 为3e5524907b43337e82a24afbc822078daf7a868f。 无论谁有任何 Git-repository-for-Git 提交,如果他们有这个提交,他们就有那么丑陋的哈希 ID,而不是其他哈希 ID。 如果他们有这个哈希 ID,它代表的是这个提交,而不是其他提交。 但是看看提交内容的第二行,上面写着父级另一个大丑陋的哈希。 此哈希 ID 标识 Git 的 Git 存储库中的另一个提交;我的这个 Git 存储库的副本中也有这个提交。 此父提交具有另一个哈希 ID(嗯,两个,因为它是合并提交),并且这些提交具有父级的哈希 ID,依此类推。

如果我们将它们绘制成一个图形,每个提交出来的箭头指向其父级,我们会得到这样的结果——好吧,让我们在这里使用一个很小的三提交存储库:

A  <-B  <-C

Git 需要知道最后一个哈希 ID;这就是分支名称的用武之地:

A  <-B  <-C   <--master

Git 使用分支名称找到的最后一个哈希 ID 来查找每个提示提交。 该提交有一个父 ID,Git 使用它来查找另一个提交,它有一个父 ID,Git 再次使用它,依此类推。 当 Git 到达像我们的提交A这样的提交时,该操作停止,它没有父 ID,因为它是图形的末尾。 这些提交称为根提交

当我们添加更多提交并链接所有这些提交时,我们会得到一些更复杂的东西,例如:

o--o--o---o--o   <-- master
    /
o--o

我们不需要内部箭头,因为我们知道它们总是向后指向:子提交知道他们的父母,但父提交不知道他们的子提交。

在一个大的存储库中,我们得到了一个非常大的图表。 但有时,根据我们如何构建图——特别是如果我们使用git add <remote>git fetch——我们可以获得具有多个根提交的存储库。 例如,在我们的小型三提交存储库中,我们可能会引入另一个包含四个提交的存储库:

A--B--C   <-- master
D--E--F--G   <-- other/master

这些提交历史记录,但现在有两个互不关联的历史记录! 从C开始,我们回到A,然后停止。 从G开始,我们回到D,然后停止。 (请记住,这些易于阅读和理解的单字母代表实际的哈希ID,它们看起来是随机的。

如果你要求 Git 合并这些,Git 所做的就是暂时组成一个没有文件的假装提交,并将其用作共同祖先:

*--A--B--C   <-- master

D--E--F--G   <-- other/master

现在历史连接起来,假祖先为了合并而暂时假装存在。 Git 现在可以将提交*的空树与提交C中的源树进行比较;提交C中的所有文件都是新添加的。 Git 还可以将空树与提交G中的源树进行比较,并且再次,那里的所有文件都是新添加的。

如果这些不相关的历史记录是主要包含相同文件的提交,则结果是一组巨大的"添加/添加冲突",因为两个提示提交添加的文件大多相同。 您可以选择执行此操作,手动解决所有冲突,然后提交。 Git 删除了假的临时根提交(实际上它甚至从未将其放入 - 空树存在于所有 Git 存储库中,因此它只是直接使用它),然后你会得到:

A--B--C----H   <-- master
/
D--E--F--G   <-- other/master

现在提交H通过连接其他不相交的子图来关联两个历史。

研究和比较本地和远程

存储库,两者都是相同的,但奇怪的例外是本地和远程之间的每次提交时作者信息都不同。不知道为什么或正确的修复是什么。

如果都相同,这表明有人专门运行git filter-branch以修改作者信息。filter-branch所做的是在应用一组文件管理器后将提交复制到新提交。 如果您选择在部分或全部提交中重写作者姓名的过滤器,则新副本是不同的提交 - 它们具有不同的author行 - 因此它们具有不同的哈希值。 如果这会更改存储库中的根提交,那么即使没有其他提交更改,所有其他复制的提交也必须记录其新的(不同的)父哈希。

例如,在我们的小型三提交存储库中,复制A但更改作者会导致一个新的哈希,我们可以将其调用A'

A--B--C   <-- master
A'

当我们下次复制 B 时,保持所有内容相同(甚至是作者),我们仍然需要将A'的 ID 放入副本中,以便副本指向A'

A'-B'

复制 C 同样会强制更改父行(如果没有别的),给我们:

A--B--C   <-- master
A'-B'-C'  [just built]

filter-branch做的最后一件事是移动所有标签以指向新副本:

A--B--C   <-- refs/original/refs/heads/master (to be deleted)
A'-B'-C'  <-- master

删除refs/original/剩余内容以忘记原始提交后,您将留下一个存储库,其中所有提交都有不同的作者,因此具有不同的哈希 ID,因此是不同的提交。

存储库是按哈希 ID 编制索引的提交集合

同样,提交历史记录。 他们的哈希 ID 是 Git 关心的。 复制存储库(通过克隆),然后使用其哈希 ID 复制提交。 通过git filter-branch或类似方式将存储库复制到新的(不同的)提交,您最终会得到一个新的、不同的存储库,具有不同的(甚至可能完全不相关的)历史记录。 (如果两个存储库都保持其根提交不变,则历史记录将是相关的。

一般来说,那些拥有旧存储库的人必须放弃他们的存储库而使用新存储库,或者决定完全忽略新存储库。 只有在您知道并接受后果的情况下,才使用这样的git filter-branch

最新更新