"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
,而是H
。 H
指向 G
,并将永远如此。 G
指向 F
,并将永远如此。我们可以复制G
和H
到新的和改进的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-branch
或 git filter-repo
执行此操作,但这很痛苦,而且您不想经常这样做。 这就是 git replace
的用武之地。
1如果用于检索对象的键与对象的哈希值相比不匹配,则数据在最初写入时发生了一些问题。散列函数对 更正 错误数据没有帮助,因此此时我们只能找到一个好的副本,大概是在另一个克隆或备份中。这就是磁盘驱动器使用 Reed-Solomon codes 而不是加密校验和的原因。 Git 在这里的工作只是 发现 腐败,而不是修复它。
2这个“最终”是维护操作。新奇的 git maintenance
命令可用于调整这些东西——这是 Git 的未来方向——但实际的删除是通过 git gc
或 git 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 --amend
在 git 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 log
或 git checkout
或任何其他 Git 命令转到 use commit F
时,它获取 提交 F'
。因此我们成功地 替换了 提交 F
而没有实际更改提交 F
.3 git log
命令特别要确保注意到发生了这种情况(查找例程将为git log
设置一个标志以供查看)并添加您看到的replaced
符号。
3请注意,这使得 git gc
和 git 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-branch
和 git filter-repo
将使 new 存储库的替换受到尊重(尽管 git --no-replace-objects filter-branch
不会,并且大概filter-repo
也有类似的事情)。因此,git replace
的一种用途是编辑历史记录,直到您希望其他人看到它为止。然后你 运行 一个无操作的过滤器操作,它“巩固了新的历史”,而不需要替换(它们现在被嵌入,原始的已经消失了)。然后您发布这个新的、不同的存储库 而不是 原始存储库。
当我 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
,而是H
。 H
指向 G
,并将永远如此。 G
指向 F
,并将永远如此。我们可以复制G
和H
到新的和改进的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-branch
或 git filter-repo
执行此操作,但这很痛苦,而且您不想经常这样做。 这就是 git replace
的用武之地。
1如果用于检索对象的键与对象的哈希值相比不匹配,则数据在最初写入时发生了一些问题。散列函数对 更正 错误数据没有帮助,因此此时我们只能找到一个好的副本,大概是在另一个克隆或备份中。这就是磁盘驱动器使用 Reed-Solomon codes 而不是加密校验和的原因。 Git 在这里的工作只是 发现 腐败,而不是修复它。
2这个“最终”是维护操作。新奇的 git maintenance
命令可用于调整这些东西——这是 Git 的未来方向——但实际的删除是通过 git gc
或 git gc --auto
完成的,在现有的 Git 用法。工作原理如下:
git gc
运行sgit 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 --amend
在 git 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 log
或 git checkout
或任何其他 Git 命令转到 use commit F
时,它获取 提交 F'
。因此我们成功地 替换了 提交 F
而没有实际更改提交 F
.3 git log
命令特别要确保注意到发生了这种情况(查找例程将为git log
设置一个标志以供查看)并添加您看到的replaced
符号。
3请注意,这使得 git gc
和 git 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-branch
和 git filter-repo
将使 new 存储库的替换受到尊重(尽管 git --no-replace-objects filter-branch
不会,并且大概filter-repo
也有类似的事情)。因此,git replace
的一种用途是编辑历史记录,直到您希望其他人看到它为止。然后你 运行 一个无操作的过滤器操作,它“巩固了新的历史”,而不需要替换(它们现在被嵌入,原始的已经消失了)。然后您发布这个新的、不同的存储库 而不是 原始存储库。