"replaced" 在 git 日志中是什么意思?

What does "replaced" mean in git log?

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

commit 3a1a6bfbd936ea441ecf1f071e82f89c7e8bbf6c (replaced, origin/main)

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

这意味着有人使用了git replace

git replace 的作用是让您告诉未来的 Git 操作,他们应该查看某个替换对象而不是某个原始对象。本段介绍替换 是如何工作的 但不会告诉您这一切 意味着什么 。问题是在这个层面上,意义还不存在。这就像说 neutron capture causes the U-235 nucleus to fission into two lighter-weight nuclei, emitting two neutrons。没错,但那又怎样?那么,核反应堆或原子弹。我们已经从干核物理走向了严重的后果。

Git 幸运的是,替换并没有那么戏剧化。但是一个简单的替换 可以 产生巨大的后果。它 您的存储库 中产生的后果不是我们可以提前确定的。我们所能做的就是描述替换背后的想法。

替换背后的想法

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

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

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

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

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

name main 指向 tip 提交,其哈希为 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 之后的每个提交都必须 also 被复制。如果有复杂链:

             I--J   <-- br1
            /
...--F--G--H   <-- main
            \
             K--L   <-- br2

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


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

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

  • git gc 运行s git reflog expire.
  • git reflog 扫描 reflogs,其中包含 reflog 条目.
  • 每个 reflog 条目都有一个日期和时间戳,以及由存储在相应 ref[=249= 中的当前哈希 ID 暗示的状态(“可达”或“不可达”) ].
  • 状态导致 git reflog expire 到两个“到期”值之一:reachable,对于从当前 ref 值可访问的提交,unreachable ,对于无法通过这种方式访问​​的提交。
  • 如果条目的期限超过到期值(默认情况下为“无法访问”30 天),reflog 条目将被删除。

这会删除对内部 Git 提交对象的最后一个实际引用,现在可以通过 git prune 删除,git gc 运行 秒后 [=52] =].因此,运行ning git commit --amendgit commit 之后立即将“修改后的”提交推到一边,由于 reflog 条目,它至少停留了 30 天:一个在 HEAD reflog 和分支 reflog 中的一个。一旦 reflog 条目消失,确实 没有 对提交的引用,并且 git prune 将 p运行e 它。


替换

用于替换的机制Git很简单。 Git 中有一个相对较低级别的例程,用于从对象数据库中获取一个 对象 ——我之前提到的 key-value store,其中键是哈希 ID,值是是对象。您将密钥提供给数据库查找代码,它会找出值。

现在,如果你允许替换——在这个级别有控制旋钮——然后当你调用“给我一个对象,我有它的哈希 ID”函数,查找函数将检查对象的哈希 ID 是否作为名称存在于 refs/replace/ 命名空间中。

因此:我们可以进行替换提交 F',这是 F 的新改进版本。这个提交有一个哈希 ID,一旦我们将它写入对象数据库。假设 F 的哈希 ID 为 aaaaaaa,而 F' 的哈希 ID 为 bbbbbbb(我将它们从 40 个字符缩短为 7 个,以便于处理,并且真正的哈希 ID 当然是随机的)。

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

当其他 Git 软件使用散列 ID aaaaaaa 调用“查找对象”功能时,该软件会注意到 refs/replace/aaaaaaa 存在。该软件 读取存储在 refs/replace/aaaaaaa 中的哈希 ID,而不是查找(和错误检查)aaaaaaa,而是查找(和错误检查) bbbbbbb 代替。然后 returns 替换对象的内容,而不是原始对象的内容。

这意味着当 git loggit checkout 或任何其他 Git 命令转到 use commit F 时,它获取 提交 F'。因此我们成功地 替换了 提交 F 而没有实际更改提交 F.3 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 您的 存储库中构建虚幻历史时,此 只会影响您的存储库 .

您也可以替换非提交对象。因为替换是一个非常低级的操作,所以您可以用它来实现各种有趣的效果。不过,它总是 local,除非您采取特殊措施将 refs/replace/ 引用也放到另一个存储库中。

请注意,使用 git filter-branchgit filter-repo 将使 new 存储库的替换受到尊重(尽管 git --no-replace-objects filter-branch 不会,并且大概filter-repo 也有类似的事情)。因此,git replace 的一种用途是编辑历史记录,直到您希望其他人看到它为止。然后你 运行 一个无操作的过滤器操作,它“巩固了新的历史”,而不需要替换(它们现在被嵌入,原始的已经消失了)。然后您发布这个新的、不同的存储库 而不是 原始存储库。