git 日志中的"替换"是什么意思?



当我做git log --all时,我在日志中发现了一个有趣的提交:

commit 3a1a6bfbd936ea441ecf1f071e82f89c7e8bbf6c (replaced, origin/main)

括号中的replaced关键字是什么意思?以及如何触发它?

这意味着有人使用了git replace.

git replace所做的是允许你告诉未来的 Git 操作,他们应该查看一些替换对象,而不是一些原始对象。 本段介绍了替换的工作原理,但没有告诉您这一切意味着什么。 问题是,在这个层面上,意义还不存在。 这就像说中子捕获导致U-235原子核裂变成两个重量较轻的原子核,发射出两个中子。 没错,但那又怎样?好吧,所以,核反应堆或原子弹。 我们已经从枯燥的核物理学走向了严重的后果。

幸运的是,Git 替换并没有那么引人注目。 但是一个简单的更换可能会产生巨大的后果。在您的存储库,它将产生的后果不是我们可以事先确定的。 我们所能做的就是描述替代品背后的想法。

替换背后的想法

任何Git 对象一旦创建,都是只读的,只要有人/某物在使用它,它就会继续存在于存储库中。 这种只读质量的原因是,在键值数据库中,每个对象都是通过其哈希 ID 找到(或寻址,用一个花哨的术语来说)的。 当 Git 从数据库中提取对象时,Git 会重新计算哈希,并验证检索到的对象的哈希是否与用于检索对象的键匹配。 这可以保证对象数据不会损坏。1

如果我们在进行新提交时犯了一个错误,现在没有其他人正在使用,并且快速检测到我们自己的错误,我们可以通过快速用新提交替换原始提交来纠正我们的错误。 我们的原始提交只能通过存储在某个分支名称中的哈希 ID 找到。 如果我们为它进行新的替换提交,纠正错误后,新提交将具有其他一些不同的哈希 ID。 我们将新的替换提交的哈希 ID 存储在分支名称(写)中,然后我们就完成了:"错误"提交仍然存在,但未使用。 由于没有人使用它,Git 最终会完全放弃它。阿拉伯数字

这对于提交来说很好,其哈希 ID 仅存储在单个分支名称中。 但是,如果提交不是那么新怎么办? 特别是,提交哈希 ID 存储在以后的提交中。 如果这个"错误"提交是提交的一部分,我们就有问题了。

请记住,提交形成向后看的链,通过指向 Git 所谓的提示提交的分支名称找到:链中的最后一个提交。 也就是说,给定一些提交系列,每个提交都有自己的哈希 ID,我们可以通过使用单个大写字母来代替哈希 ID 来绘制它们:

... <-F <-G <-H   <--main

名称main指向提示提交,其哈希值为H。 该提交向后指向较早的提交G。 将G点向后提交到较早的提交F,依此类推。

如果提交F有错误,我们可以尝试做git commit --amend做的事情:创建一个新的和改进的F',然后把F推开:

F ...
/
... <-F'

但是当我们这样做时,现有的提交G(实际上包含现有提交F的哈希 ID,并且无法更改)仍然指向F

F <-G <-H   <--main
/
... <-F'

我们简单地尝试修改F是行不通的,因为main点,不是为了F,而是为了HH指向G,并将永远这样做。G指向F,并将永远这样做。我们可以GH复制到新的和改进的G'H'

F <-G <-H   <--main
/
... <-F' <-G' <-H'

制作了份副本后,我们现在可以将分支名称重新指向main

F <-G <-H
/
... <-F' <-G' <-H'   <--main

这就是git rebase所做的。 但它的缺点是F之后的每个提交也必须复制。 如果有复杂的链:

I--J   <-- br1
/
...--F--G--H   <-- main

K--L   <-- br2

整个事情迅速成为重写历史的噩梦,需要移动多个分支名称。您可以使用git filter-branchgit filter-repo来做到这一点,但这很痛苦,不是您想经常做的事情。这就是git replace的用武之地。


1如果用于检索对象的键与对象的哈希不匹配,则数据自最初写入以来发生了一些问题。 哈希函数在纠正错误数据方面没有帮助,因此在这一点上,我们只能找到一个好的副本,大概是在另一个克隆或备份中。 这就是为什么磁盘驱动器使用Reed-Solomon代码而不是加密校验和的原因。 Git 在这里的工作只是发现腐败,而不是修复它。

2这个"最终"是一个维护操作。 新奇的git maintenance命令可以用来调整这些东西——这是 Git 的未来方向——但实际的删除是通过git gcgit gc --auto完成的,在现有的 Git 使用中。 其工作原理如下:

  • git gc运行git reflog expire.
  • git reflog扫描包含引用日志条目引用日志
  • 每个 reflog 条目都有一个日期和时间戳,以及存储在相应ref中的当前哈希 ID 所隐含的状态("可访问"或"无法访问")。
  • 该状态导致git reflog expire两个"到期"值之一:可到达,对于可从当前 ref 值访问的提交,以及无法访问,对于无法以这种方式访问的提交。
  • 如果条目的期限超过到期值(默认情况下,"无法访问"为 30 天),则会删除 reflog 条目。

这会删除对内部 Git 提交对象的最后一个实际引用,现在可以通过git prune删除该对象,git gcgit reflog expire后运行。 因此,在git commit之后立即运行git commit --amend会将"修正"提交推到一边,由于 reflog 条目,它至少会徘徊 30 天:一个在HEADreflog 中,一个在分支 reflog 中。 一旦 reflog 条目消失,实际上就没有对提交的引用,git prune会修剪它。

<小时 />

更换

Git 用于替换的机制很简单。 Git 中有一个相对低级的例程,用于从对象数据库(我前面提到的键值存储)中获取对象,其中键是哈希 ID,值是对象。 你把键交给数据库查找代码,它会找出值。

现在,如果您允许替换(在此级别有控制旋钮),那么当您调用"给我一个对象,我有它的哈希 ID"函数时,查找函数将检查对象的哈希 ID 是否作为名称存在于refs/replace/命名空间中。

所以:我们可以制作一个替换提交F',这是一个新的和改进的F版本。 一旦我们将其写入对象数据库,此提交就会有一个哈希 ID。 假设F有哈希 IDaaaaaaaF'有哈希 IDbbbbbbb(我将它们从 40 个字符缩短到 7 个字符以使它们更容易处理,真正的哈希 ID 当然是随机的)。

我们现在将哈希 IDbbbbbbb存储在名称refs/replace/aaaaaaa. 也就是说,提交F的哈希 ID,无论它是什么,都将成为refs/replace/名称。 在该名称中,我们存储替换提交的哈希 ID,此处bbbbbbb.

当其他一些 Git 软件调用带有哈希 IDaaaaaaa的"查找对象"函数时,该软件会注意到refs/replace/aaaaaaa存在。 该软件读取存储在refs/replace/aaaaaaa中的哈希 ID,而不是查找(和错误检查)aaaaaaa,而是查找(和错误检查)bbbbbbb。 然后,它返回替换对象的内容,而不是原始对象的内容。

这意味着当git loggit checkout或任何其他 Git 命令使用提交F时,它会得到提交F'。 因此,我们已经成功地替换了提交F,而没有实际更改提交F3特别是git log命令确保注意到发生了这种情况(查找例程将设置一个标志供git log查看),并添加您看到的replaced表示法。


3请注意,这使得git gcgit prune必须更加努力地工作,因为对象F仍然被"真实"引用,而F'则通过refs/replace/名称引用。 幸运的是,git gc在禁用替换的情况下运行就足够了。


看到现实,以及为什么这很重要

如果您想查看数据库中的真实内容,无需替换,则可以运行git --no-replace-objects log。 这将使git log调用"获取对象"函数,并禁用替换。 您将看到原始历史记录,而不是替换的历史记录。

要查看替换对象,请使用git replace --list(或不带参数的git replace,表示--list),或在软件中使用git for-each-ref refs/replace

请注意,克隆存储库时,克隆过程通常不会复制refs/replace/命名空间。 默认情况下,使用git push也不会复制refs/replace/名称。 因此,当您使用git replace在存储库中构建虚幻的历史记录时,这只会影响您的存储库

您也可以替换非提交对象。 因为替换是一个低级的操作,你可以用它来制作各种有趣的效果。 不过,它始终是本地的,除非您采取特殊操作将refs/replace/引用也放入另一个存储库。

请注意,使用git filter-branchgit filter-repo将使具有替换的新存储库得到尊重(尽管git --no-replace-objects filter-branch不会,并且可能与filter-repo有类似的事情)。 因此,git replace的一个用途是编辑历史记录,直到您看到它看起来像您希望其他人看到它的方式。 然后,运行一个原本无操作的筛选器操作,该操作可以像以前一样"巩固新的历史记录",而无需替换(它们现在已嵌入,原始文件刚刚消失)。 然后,您可以发布这个新的、不同的存储库,而不是原始存储库。

最新更新