Git是如何快速创建提交记录的?

3
据我所知,Git 中的每个提交都是整个仓库的“快照”,这意味着至少需要读取每个文件。我的仓库大小为 9.2 GB,但提交只需几分之一秒的时间,这看起来毫无道理。
3个回答

9
至少,每个文件都必须被读取。
相反,那是可能发生的最多的事情。
运行git commit以提交您暂存的更改通常很快,因为实际上暂存更改已经完成了大部分工作。创建提交只是将索引(也称为“暂存区”)转换为非常轻量级的提交对象,其中包含有关您提交的元数据和一些树对象,这些树对象包含存储库的结构。
然而,当您在特定文件上运行git add时,所有文件中的数据都会添加到git的数据库中。该文件的数据随后存储在暂存区中,以便在运行git commit时,有关该文件的所有信息已经在索引中。因此,代价最高的部分在运行git add时得到了摊销。
另一个微妙之处在于,索引包含有关存储库中所有文件的信息,并维护有关工作目录的信息,例如它上次检查文件的时间戳和文件大小。因此,即使您运行像git add .这样的命令来暂存所有更改的文件,它只需要stat文件以找出它是否更改,如果没有更改,则可以忽略它。
显然,查看工作目录中的所有文件有点昂贵,但比添加完整快照的未更改文件便宜得多
因此,即使git在每个提交中存储存储库的快照,它实际上只需要为更改的文件存储新数据,对于其他所有内容,它可以存储指向旧的、未更改的文件内容的指针。

3
注意:如果您有一个包含大量提交的存储库,比如“地球上最大的Git存储库”,拥有超过250,000个提交,那么添加新的提交实际上可能会很慢。
这就是为什么Git 2.23(2019年第三季度)引入了提交图链的原因。
查看提交 5b15eb3, 提交 16110c9, 提交 a09c130, 提交 e2017c4, 提交 ba41112, 提交 3da4b60, 提交 c2bc6e6, 提交 8d84097, 提交 c523035, 提交 1771be9, 提交 135a712, 提交 6c622f9, 提交 144354b, 提交 118bd57, 提交 5c84b33, 提交 3cbc6ed, 提交 d4f4d60, 提交 890345a (2019年6月18日) 由Derrick Stolee (derrickstolee)合并。
(由Junio C Hamano -- gitster --提交 92b1ea6中合并,2019年7月19日)

commit-graph: 记录提交图链

现在的文档包括:

提交图链

通常情况下,存储库以接近恒定的速度增长(每天的提交数)。 随着时间的推移,通过获取操作添加的提交数量要比完整历史记录中的提交数量小得多。

通过创建“链式”提交图,我们可以在不重写整个提交历史的情况下快速写入新的提交数据 ——至少大部分时间是这样的。

文件布局

提交图链使用多个文件,并且我们使用固定的命名约定来组织这些文件。 每个提交图文件的名称为 $OBJDIR/info/commit-graphs/graph-{hash}.graph,其中的 {hash} 是该文件底部存储的十六进制哈希值(该哈希值是该哈希之前的文件内容的哈希)。 对于一条提交图链,一个纯文本文件位于 $OBJDIR/info/commit-graphs/commit-graph-chain,按从“最低”到“最高”的顺序包含这些文件的哈希值。

例如,如果 commit-graph-chain 文件包含以下行:

{hash0}
{hash1}
{hash2}

那么提交图链看起来如下所示:

+-----------------------+
|  graph-{hash2}.graph  |
+-----------------------+
    |
+-----------------------+
|                       |
|  graph-{hash1}.graph  |
|                       |
+-----------------------+
    |
+-----------------------+
|                       |
|                       |
|                       |
|  graph-{hash0}.graph  |
|                       |
|                       |
|                       |
+-----------------------+

  • X0graph-{hash0}.graph 中的提交数目,
  • X1graph-{hash1}.graph 中的提交数目,以及
  • X2graph-{hash2}.graph 中的提交数目。

如果一个提交在 graph-{hash2}.graph 中出现在位置 i,则我们将其解释为位于位置 (X0 + X1 + i) 的提交,并且这将被用作它的“图位置”。 graph-{hash2}.graph 中的提交使用这些位置来引用它们的父提交,而父提交可能位于 graph-{hash1}.graphgraph-{hash0}.graph 中。 我们可以通过检查它在区间 [0, X0)[X0, X0 + X1)[X0 + X1, X0 + X1 + X2) 中的包含情况来导航到位于任意位置 j 的提交。


那意味着git commit-grah有一个新的write命令选项:--split

commit-graph: 在内置命令中添加--split选项

在'git commit-graph write'子命令中添加一个新的"--split"选项。
该选项允许可选行为,即写入一个commit-graph链。

当前行为将添加一个包含任何不在现有commit-graph或commit-graph链中的提交的tip commit-graph。
以后的更改将允许合并链条并过期过时的文件。

添加一个新的测试脚本(t5324-split-commit-graph.sh),演示了这个行为。

而且相同的文档还补充道:
使用--split选项,将提交图作为多个存储在<dir>/info/commit-graphs目录下的提交图文件链。
新的提交(不在提交图中的)会被添加到一个新的“tip”文件中。
如果满足以下合并条件,则将此文件与现有文件合并:

  • 如果未指定--size-multiple=<X>,则令X等于2。如果新的tip文件有N个提交,而之前的tip文件有M个提交,并且X乘以N大于M,则将两个文件合并为一个文件。

  • 如果使用--max-commits=<M>指定了正整数M,并且新的tip文件将有超过M个提交,则将新的tip与之前的tip合并。

最后,如果未指定--expire-time=<datetime>,则将datetime设为当前时间。在写入拆分的提交图后,删除所有修改时间早于datetime的未使用的提交图。


这将有助于处理分叉

commit-graph:允许跨交替链

在像分叉网络这样的环境中,拥有一个跨越基础仓库和分叉仓库的提交图链是很有帮助的。
分叉通常是大型仓库上的一小部分数据,但有时分叉可能要大得多。
例如,git-for-windows/git 的提交数量几乎是 git/git 的两倍,因为它在每个主要版本更新时都会变基其提交。

文档 现在包括:

跨多个对象目录的链

在具有备用存储库的情况下,我们会从本地对象目录开始查找“commit-graph-chain”文件,然后再查找每个备用存储库。
存在的第一个文件定义了我们的链。
当我们为链文件中的每个哈希寻找“graph-{hash}”文件时,我们对主机目录遵循相同的模式进行搜索。
这样可以将提交图分割成多个分叉网络中的分支。
典型情况是一个大型的“基础”存储库和许多较小的分叉存储库。
随着基础存储库的更新,它可能会比分叉存储库更频繁地更新和合并其提交图链。
如果分叉存储库在基础存储库之后更新其提交图,则应将提交图链“重新父化”到基础存储库中的新链上。
在读取每个“graph-{hash}”文件时,我们跟踪包含它的对象目录。在写入新的提交图文件时,我们检查源对象目录中是否有任何更改,并读取该源的“commit-graph-chain”文件,然后基于这些文件创建一个新文件。
在此“重新父化”操作期间,我们必须将分叉中的所有级别折叠,因为所有文件都与新的基础文件无效。
这也涉及到过期的提交图文件:
commit-graph:过期提交图文件
当我们在提交图链中合并提交图文件时,我们应该清理不再使用的文件。
此更改引入了一个“expiry_window”值到上下文中,该值始终为零(目前为止)。 然后,我们检查“$OBJDIR/info/commit-graphs”文件夹中每个“graph-{hash}.graph”文件的修改时间,并取消链接早于“expiry_window”的文件。 documentation现在引用以下内容:
删除图-{hash}文件
在写入新的提示文件之后,一些graph-{hash}文件可能不再是链的一部分。重要的是要及时从磁盘中删除这些文件。
延迟删除的主要原因是另一个进程可能在重写commit-graph-chain文件之前读取它,但在删除graph-{hash}文件后仍然查找它们。
为了在取消引用后保留旧的拆分提交图一段时间, 当这些文件变得无引用时,我们会更新文件的修改时间。
然后,我们扫描$OBJDIR/info/commit-graphs/目录下修改时间早于给定过期窗口的graph-{hash} 文件。
该窗口默认为零,但可以使用命令行参数或配置设置进行更改。
在Git 2.27(2020年第二季度)中,"git commit-graph write" 学会了以不同的方式写出分割文件。
查看commit dbd5e0a(2020年4月29日)由Junio C Hamano(gitster提交。
查看commit 7a9ce02(2020年4月15日),以及commit 6830c36commit f478106commit 8a6ac28commit fdbde82commit 4f02735commit 2fa05f3(2020年4月14日)由Taylor Blau(ttaylorr提交。
(由Junio C Hamano -- gitster --合并于commit 6a1c17d,2020年5月1日)

builtin/commit-graph.c:引入拆分策略“no-merge”

签名作者:Taylor Blau

在之前的提交中,我们为支持不同的拆分策略打下了基础。在这个提交中,我们引入了第一个拆分策略:“'no-merge'”。

传递“--split=no-merge”对于希望写入新的增量提交图但又不想花费精力压缩增量链路的调用者来说非常有用(*1)。

以前,可以通过传递'--size-multiple=0'来实现这一点,但自从63020f175f("commit-graph: prefer default size_mult when given zero",2020-01-02,Git v2.25.0-rc2 -- merge)之后,情况已经改变。
当给出'--split=no-merge'时,提交图机制将永远不会压缩现有链,并且它将始终写入新的增量。
(*1):例如,当服务器管理员在每次推送后运行某个程序时,可能希望确保每个作业的运行时间与推送的大小成比例,而不是在提交图机制决定触发合并时出现“跳跃”。
"

"git fsck --no-progress"(man) still spewed noise from the commit-graph subsystem, which has been corrected with Git 2.42 (Q3 2023).

"

查看 提交 9281cd0, 提交 7248857, 提交 f5facaa, 提交 eb319d6, 提交 39bdd30, 提交 eda206f (2023年7月7日) 由 Taylor Blau (ttaylorr) 进行。
(由 Junio C Hamano -- gitster -- 合并于 提交 6016ee0, 2023年7月18日)

commit-graph.c:在verify期间避免重复的进度输出
Signed-off-by: Taylor Blau Acked-by: Derrick Stolee

When git commit-graph(man) verify was taught how to verify commit-graph chains in 3da4b60 ("commit-graph: verify chains with --shallow mode", 2019-06-18, Git v2.23.0-rc0 -- merge listed in batch #6), it produced one line of progress per layer of the commit-graph chain.

$ git.compile commit-graph verify
Verifying commits in commit graph: 100% (4356/4356), done.
Verifying commits in commit graph: 100% (131912/131912), done.

This could be somewhat confusing to users, who may wonder why there are multiple occurrences of "Verifying commits in commit graph".

There are likely good arguments on whether or not there should be one line of progress output per commit-graph layer.
On the one hand, the existing output shows us verifying each individual layer of the chain.
But on the other hand, the fact that a commit-graph may be stored among multiple layers is an implementation detail that the caller need not be aware of.

Clarify this by showing a single progress meter regardless of the number of layers in the commit-graph chain.
After this patch, the output reflects the logical contents of a commit-graph chain, instead of showing one line of output per commit-graph layer:

$ git.compile commit-graph verify
Verifying commits in commit graph: 100% (136268/136268), done.

0
就我目前的理解而言...假设您在主分支中有许多提交,并且另一个分支也有很多提交。因此,如果VCS不支持具有哈希等内容的git概念,而只存储文件差异,然后您想创建分支。那么另一个VCS要么必须恢复共享提交之前的所有更改并应用其他分支的所有更改,要么必须逐个比较所有文件。在我看来,git的散列算法似乎是更好的方法,即使git必须进行大量迭代/搜索,我也这样认为。我不知道我是否正确,今天我刚开始阅读关于git的东西。请随意点赞/踩和评论 :D 我认为这是一个只有很少人真正拥有深入了解的话题

2
你好!为了以后在 Stack Overflow 上更好地表现,查看 回答问题的格式 将是一个不错的选择。- 谢谢 - Momin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接