理解Git提交实现方式对我来说很简单,但理解他人对提交的看法却有难度。因此,我在Mastodon上向他人提出了一些问题。
我进行了一个 非常不科学的调查,询问大家是怎么看待 Git 提交的:是快照、差异,还是所有之前提交的列表?(当然,把它看作这三者都是合理的,但我很好奇人们的 主要
结果是:
我很惊讶差异和快照两个选项的比例如此接近。人们还提出了一些有趣但相互矛盾的观点,比如
“在我看来,提交是一个差异,但我认为它实际上是以快照的形式实现的” 和
“在我看来,提交是一个快照,但我认为它实际上是以差异的形式实现的”。关于提交的实际实现方式,我们稍后再详谈。
在我们进一步讨论之前:我们的说 “一个差异” 或 “一个快照” 都是什么意思?
我说的“差异”可能相当明显:差异就是你在运行 git show COMMIT_ID
时得到的东西。例如,这是一个 rbspy 项目中的拼写错误修复:
diff --git a/src/ui/summary.rs b/src/ui/summary.rs index 5c4ff9c..3ce9b3b 100644 --- a/src/ui/summary.rs +++ b/src/ui/summary.rs @@ -160,7 +160,7 @@ mod tests { "; let mut buf: Vec = Vec::new(); -stats.write(&mut buf).expect("Callgrind write failed"); +stats.write(&mut buf).expect("summary write failed"); let actual = String::from_utf8(buf).expect("summary output not utf8"); assert_eq!(actual, expected, "Unexpected summary output"); }
你可以在 GitHub 上看到它: https://github.com/rbspy/rbspy/commit/24ad81d2439f9e63dd91cc1126ca1bb5d3a4da5b
我说的 “快照” 是指 “当你运行 git checkout COMMIT_ID
时得到的所有文件”。
Git 通常将提交的文件列表称为 “树”(如“目录树”),你可以在 GitHub 上看到上述提交的所有文件:
https://github.com/rbspy/rbspy/tree/24ad81d2439f9e63dd91cc1126ca1bb5d3a4da5b(它是 /tree/
而不是 /commit/
)
我最常听到的关于学习 Git 的建议大概是 “只要学会 Git 在内部是如何表示事物的,一切都会变得清晰明了”。我显然非常喜欢这种观点(如果你花了一些时间阅读这个博客,你就会知道我 喜欢
但是作为一个学习 Git 的方法,它并没有我希望的那么成功!通常我会兴奋地开始解释 “好的,所以 Git
提交是一个快照,它有一个指向它的父提交的指针,然后一个分支是一个指向提交的指针,然后……”,但是我试图帮助的人会告诉我,他们并没有真正发现这个解释有多有用,他们仍然不明白。所以我一直在考虑其他方案。
但是让我们还是先谈谈内部实现吧。
在内部,Git 将提交表示为快照(它存储每个文件当前版本的 “树”)。我在 在一个 Git 仓库中,你的文件在哪里? 中写过这个,但下面是一个非常快速的内部格式概述。
这是一个提交的表示方式:
$ git cat-file -p 24ad81d2439f9e63dd91cc1126ca1bb5d3a4da5b tree e197a79bef523842c91ee06fa19a51446975ec35 parent 26707359cdf0c2db66eb1216bf7ff00eac782f65 author Adam Jensen1672104452 -0500 committer Adam Jensen1672104890 -0500 Fix typo in expectation message
以及,当我们查看这个树对象时,我们会看到这个提交中仓库根目录下每个文件/子目录的列表:
$ git cat-file -p e197a79bef523842c91ee06fa19a51446975ec35 040000 tree 2fcc102acd27df8f24ddc3867b6756ac554b33ef.cargo 040000 tree 7714769e97c483edb052ea14e7500735c04713eb.github 100644 blob ebb410eb8266a8d6fbde8a9ffaf5db54a5fc979a.gitignore 100644 blob fa1edfb73ce93054fe32d4eb35a5c4bee68c5bf5ARCHITECTURE.md 100644 blob 9c1883ee31f4fa8b6546a7226754cfc84ada5726CODE_OF_CONDUCT.md 100644 blob 9fac1017cb65883554f821914fac3fb713008a34CONTRIBUTORS.md 100644 blob b009175dbcbc186fb8066344c0e899c3104f43e5Cargo.lock 100644 blob 94b87cd2940697288e4f18530c5933f3110b405bCargo.toml
这意味着检出一个 Git 提交总是很快的:对 Git 来说,检出昨天的提交和检出 100 万个提交之前的提交一样容易。Git 永远不需要重新应用 10000 个差异来确定当前状态,因为提交根本就不是以差异的形式存储的。
我刚刚提到了 Git 提交是一个快照,但是,当有人说 “在我看来,提交是一个快照,但我认为它在实现上是一个差异”
时,这其实也是对的!Git
提交并不是以你可能习惯的差异的形式表示的(它们不是以与上一个提交的差异的形式存储在磁盘上的),但基本的直觉是,如果你要对一个 10,000
行的文件编辑 500 次,那么存储 500 份文件的效率会很低。
Git 有一个将文件以差异的形式存储的方法。这被称为 “packfile”,Git 会定期进行垃圾回收,将你的数据压缩成 packfile 以节省磁盘空间。当你 git clone
一个仓库时,Git 也会压缩数据。
这里,我没有足够的篇幅来完整地解释 packfile 是如何工作的(Aditya Mukerjee 的 《解压 Git packfile》是我最喜欢的解释它们是如何工作的文章)。不过,我可以在这里简单总结一下我对 deltas 工作原理的理解,以及它们与 diff 的区别:
当我们运行 git show SOME_COMMIT
来查看某个提交的差异时,实际上发生的事情有点反直觉。我的理解是:
所以,Git 会将变化量转换为快照,然后计算差异。它感觉有点奇怪,因为它从一个类似差异的东西开始,最终得到另一个类似差异的东西,但是变化量和差异实际上是完全不同的,所以这是说得通的。
也就是说,我认为 Git 将提交存储为快照,而 packfile 只是一个实现细节,目的是节省磁盘空间并加快克隆速度。我其实从来没必要知道 packfile 是如何工作的,但它确实能帮助我理解 Git 是如何在不占用太多磁盘空间的情况下将提交快照化的。
我认为一个相当常见的,对 Git 的 “错误” 的理解是:
这个理解当然是错误的(在现实中,提交是以快照的形式存储的,差异是从这些快照计算出来的),但是对我来说它似乎非常有用而且有意义!在考虑合并提交时会有一点奇怪,但是或许我们可以说这只是基于合并提交的第一个父提交的差异。
我认为这个错误的理解有的时候非常有用,而且对于日常 Git 使用来说它似乎并没有什么问题。我真的很喜欢它将我们最常使用的东西(差异)作为最基本的元素——它对我来说非常直观。
我也一直在思考一些其他有用但 “错误” 的 Git 理解,比如:
我认为有一系列非常有意义的、 “错误” 的对 Git 的理解,它们在很大程度上都受到 Git 用户界面的支持,并且在大多数情况下都不会产生什么问题。但是当你想要撤销一个更改或者出现问题时,它可能会变得混乱。
就算我知道在 Git 中提交是快照,我可能大部分时间也都将它们视为差异,因为:
git show
时,你会看到差异,所以这只是我习惯看到的东西但是我有时也会将提交视为快照,因为:
git checkout COMMIT_ID
在做什么(重新应用 10000 个提交的想法让我感到很有压力)Mastodon 的一些回复中还提到了:
人们在谈论提交时使用的其他一些词可能不那么含糊:
我很难了解人们对 Git 有哪些不同的理解。尤其棘手的是,尽管 “错误” 的理解往往非常有用,但人们却非常热衷于警惕 “错误”
的心智模式,所以人们不愿意分享他们 “错误” 的想法,生怕有什么 Git 解释者会站出来向他们解释为什么他们是错的。(这些 Git
解释者通常是出于善意的,但是无论如何它都会产生一种负面影响)
但是我学到了很多!我仍然不完全清楚该如何谈论提交,但是我们最终会弄清楚的。
感谢 Marco Rogers、Marie Flanagan 以及 Mastodon 上的所有人和我讨论 Git 提交。
以上是Git 提交是差异、快照还是历史记录?的详细内容。更多信息请关注PHP中文网其他相关文章!