发布于 2026-01-06 2 阅读
0

我用 Rust 自己写了一个 Git 来理解版本控制

我用 Rust 自己写了一个 Git 来理解版本控制

我使用 Git 已经好几年了。提交、推送、拉取,偶尔遇到问题也会手忙脚乱。但如果你问我运行 `git commit` 到底发生了什么git commit,我只会含糊其辞地回答“保存更改”,然后希望你不要再追问了。

这让我很困扰。所以我用 Rust 写了自己的版本控制系统Veridian。这并非因为世界需要另一个 Git,而是因为我需要理解我们现有的版本控制系统。

事实证明,Git 比我想象的要简单得多。

为什么每个人都觉得 Git 令人困惑

我们学习 Git 的方式是本末倒置。我们死记硬背命令,却不理解它们的作用。git add暂存文件。好吧,但暂存到底是什么意思?git commit保存你的工作。不错,但保存到哪里,又如何保存呢?

我花了多年时间只是机械地执行命令。我会用 Git,但却无法理解它的运作原理。创建 Veridian 改变了这一切。

这就是 Git 的真正含义

抛开所有命令和功能,Git 本质上就是一个内容寻址存储系统。听起来很复杂,但其实不然。

你的存储系统是按内容而非名称保存数据的。两次输入相同的内容?存储到同一个位置。更改一个字符?存储到不同的位置。

这就是 Git。“location”是 SHA-1 哈希值。“storage”是文件.git/objects夹。

三种类型的物体

Blob 指的是文件内容。具体来说,就是给你的文件添加一个类似 `.blob` 的头部blob <size>\0,然后进行哈希处理、压缩和存储。搞定。Git 在这里并不关心文件名,只关心文件内容。

树状结构是目录列表。它们通过列出文件和文件夹及其哈希值来表示“这个文件夹以前的样子”。树状结构可以指向二进制大对象(blob)和其他树状结构。

提交是带有上下文的快照。每个提交都指向一个树状结构(你的项目之前的状态),指向父提交(之前的代码),并包含作者、时间和消息等元数据。

三种对象类型。这就是整个系统。

为什么哈希系统很智能

同一个文件在多个提交中出现?只存储一次。大文件中只修改了一行?只存储新版本。想检查两个文件是否相同?比较哈希值,立即得到答案。

Git 并不会反复复制你的项目。它存储的是独特的代码片段,并从中构建快照。这就是为什么拥有数百次提交的仓库体积并不大的原因。

分支只是文件

分支实际上就是一个包含提交哈希值的文件。

该文件.git/refs/heads/main包含 40 个字符,即你最近一次提交的哈希值。当你创建一个分支时,Git 会写入一个包含当前提交哈希值的新文件。当你提交时,Git 会用新的哈希值更新该文件。

无需复制文件,只需更新一个小型文本文件。这就是分支“轻量级”的原因。

压缩部分很酷

Git 使用 zlib 在存储之前压缩所有内容。因此,你的目标文件并非原始内容,而是经过压缩的。我在开发 Veridian 时,必须处理每次读写操作的压缩和解压缩。

具体过程如下:Git 会获取你的 blob 数据(包含头部信息),对其进行压缩,然后将其存储在 `<path>` 目录中,.git/objects/ab/cdef123...其中ab`<path>` 是哈希值的前两个字符,` cdef123...<path>` 是剩余部分。之所以采用两个字符的分割方式,是为了避免在一个目录中出现成千上万个文件,从而降低文件系统的运行速度。

读取文件需要找到文件,用 zlib 解压缩,解析文件头检查对象类型和大小,然后返回文件内容。Rust 的标准库没有内置 zlib,所以我用了 zlib crate flate2。整个过程只用了五行代码。

维瑞迪安大厦教会了我什么

我原以为构建版本控制系统会很难,结果并非如此。

维里迪安建筑

用 Rust 构建程序很有意思,因为 Rust 会让你思考内存和所有权的问题。当你对文件进行哈希运算和构建目录树时,你需要正确处理错误(如果文件不存在怎么办?),并谨慎管理缓冲区(你不能直接把一个 5GB 的文件加载到内存中)。

但说实话,版本控制逻辑本身很简单。我的代码大部分都是读取文件、计算 SHA-1 哈希值,然后写入压缩数据。难点不在于算法,而在于理解 Git 实际在做什么。

初始化命令

创建一个.veridian文件夹。添加用于存放对象和引用的子文件夹。创建一个 HEAD 文件。完成,你就拥有了一个代码仓库。

哈希对象命令

读取文件,添加文件头,使用 SHA-1 算法对其进行哈希处理,使用 zlib 压缩,然后写入目标位置.veridian/objects/。返回哈希值。这就是文件进入系统的方式。

Write-Tree 命令

遍历目录。对每个文件进行哈希运算(生成二进制数据块)。将所有文件名和哈希值放入一个树状对象中。再次对该树状对象进行哈希运算。现在你就得到了目录的快照。

我学到的一点是:目录树条目需要按文件名排序。如果不排序,同样的目录结构会因为文件处理顺序的不同而产生不同的哈希值。Git 会自动排序以保持哈希值的一致性。虽然是个小细节,但很重要。

提交树命令

获取树哈希值。如果存在父提交,则添加父提交哈希值。添加作者信息和时间。添加提交消息。对所有内容进行哈希处理。写入对象。更新分支指针。更新 HEAD。这就是一次提交。

它的实现非常简洁。它的工作原理和 Git 类似,因为 Git 本身就非常简单。

有趣的是:Git 将时间戳存储为 Unix 时间戳(自 1970 年以来的秒数),并包含时区信息。因此,提交对象包含类似 ` <time_tamp_id>` 的信息1760211794 +0530,其中包含时间戳和时区偏移量。当你执行 ` git commit` 时git commit,它会获取你的系统时间和时区。我使用了 Rust 的 ` chronogist` crate 来实现这一点,但你也可以使用任何语言。

终于明白的事情

为什么 Git 速度快:它比较的是哈希值,而不是文件内容。40 个字符的字符串。速度超快。

为什么会出现分离的 HEAD 指针: HEAD 通常指向一个分支文件,而该分支文件又指向一个提交。如果直接检出一个提交,HEAD 指针就会直接指向该提交,跳过分支。此时 HEAD 指针就会分离,因为你当前不在分支上,而是在某个特定的提交上。

为什么可以恢复已删除的提交:它们仍然存在.git/objects,只是未被引用。使用git reflog`get_hash ...

为什么会出现合并冲突:两个提交拥有相同的父提交,但对同一个文件进行了不同的更改。Git 无法决定哪个提交生效,需要您手动决定。

我学到了什么

Git难用不是因为它复杂,而是因为我们学习方法不对。

一旦你理解了 Git 是一个键值存储系统,其中键是内容哈希,值有三种类型(blob、tree、commit),一切就都明白了。分支是指针。合并操作会合并树。变基操作会将提交重新应用到不同的父分支上。

我用 Git 好几年都没搞懂。后来我花了一周时间搭建了 Veridian,突然间 Git 就变得有意义了。不是因为搭建过程有什么神奇之处,而是因为它迫使你理解代码运行的原理。

为什么你应该尝试一下

你不需要打造完美的东西,开始就好。

创建一个代码仓库。将文件存储为 blob 对象。构建一个代码树。提交一次代码。完成这四件事,你对 Git 的理解就会超过大多数开发者。

构建 Veridian 大概花了一周时间。现在我用 Git 的时候,我终于知道发生了什么。它只是数据结构和文件操作,没什么复杂的。

Veridian 并不完美。它缺少一些功能,可能还有 bug。但它教会了我 Git 的工作原理,而这才是重点。

如果你想真正学习 Git,而不仅仅是使用它,那就动手做点什么。哪怕只是个小项目,哪怕会出错。花一周时间实践,你学到的东西远比花几个月时间阅读文档要多得多。

去GitHub上看看Veridian。把它搞坏,修好,从中学习。这就是它的运作方式。

文章来源:https://dev.to/kayleecodez/i-built-git-from-scratch-to-finally-understand-what-ive-been-using-for-years-37a9