当我做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
,而是为了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
之后的每个提交也必须复制。 如果有复杂的链:
I--J <-- br1
/
...--F--G--H <-- main
K--L <-- br2
整个事情迅速成为重写历史的噩梦,需要移动多个分支名称。您可以使用git filter-branch
或git filter-repo
来做到这一点,但这很痛苦,不是您想经常做的事情。这就是git replace
的用武之地。
1如果用于检索对象的键与对象的哈希不匹配,则数据自最初写入以来发生了一些问题。 哈希函数在纠正错误数据方面没有帮助,因此在这一点上,我们只能找到一个好的副本,大概是在另一个克隆或备份中。 这就是为什么磁盘驱动器使用Reed-Solomon代码而不是加密校验和的原因。 Git 在这里的工作只是发现腐败,而不是修复它。
2这个"最终"是一个维护操作。 新奇的git maintenance
命令可以用来调整这些东西——这是 Git 的未来方向——但实际的删除是通过git gc
或git gc --auto
完成的,在现有的 Git 使用中。 其工作原理如下:
git gc
运行git reflog expire
.git reflog
扫描包含引用日志条目的引用日志。- 每个 reflog 条目都有一个日期和时间戳,以及存储在相应ref中的当前哈希 ID 所隐含的状态("可访问"或"无法访问")。
- 该状态导致
git reflog expire
两个"到期"值之一:可到达,对于可从当前 ref 值访问的提交,以及无法访问,对于无法以这种方式访问的提交。 - 如果条目的期限超过到期值(默认情况下,"无法访问"为 30 天),则会删除 reflog 条目。
这会删除对内部 Git 提交对象的最后一个实际引用,现在可以通过git prune
删除该对象,git gc
git reflog expire
后运行。 因此,在git commit
之后立即运行git commit --amend
会将"修正"提交推到一边,由于 reflog 条目,它至少会徘徊 30 天:一个在HEAD
reflog 中,一个在分支 reflog 中。 一旦 reflog 条目消失,实际上就没有对提交的引用,git prune
会修剪它。
更换
Git 用于替换的机制很简单。 Git 中有一个相对低级的例程,用于从对象数据库(我前面提到的键值存储)中获取对象,其中键是哈希 ID,值是对象。 你把键交给数据库查找代码,它会找出值。
现在,如果您允许替换(在此级别有控制旋钮),那么当您调用"给我一个对象,我有它的哈希 ID"函数时,查找函数将检查对象的哈希 ID 是否作为名称存在于refs/replace/
命名空间中。
所以:我们可以制作一个替换提交F'
,这是一个新的和改进的F
版本。 一旦我们将其写入对象数据库,此提交就会有一个哈希 ID。 假设F
有哈希 IDaaaaaaa
,F'
有哈希 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 log
或git checkout
或任何其他 Git 命令使用提交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
在存储库中构建虚幻的历史记录时,这只会影响您的存储库。
您也可以替换非提交对象。 因为替换是一个低级的操作,你可以用它来制作各种有趣的效果。 不过,它始终是本地的,除非您采取特殊操作将refs/replace/
引用也放入另一个存储库。
请注意,使用git filter-branch
和git filter-repo
将使具有替换的新存储库得到尊重(尽管git --no-replace-objects filter-branch
不会,并且可能与filter-repo
有类似的事情)。 因此,git replace
的一个用途是编辑历史记录,直到您看到它看起来像您希望其他人看到它的方式。 然后,运行一个原本无操作的筛选器操作,该操作可以像以前一样"巩固新的历史记录",而无需替换(它们现在已嵌入,原始文件刚刚消失)。 然后,您可以发布这个新的、不同的存储库,而不是原始存储库。