Skip to content

Commit

Permalink
docs: Added alert boxes
Browse files Browse the repository at this point in the history
  • Loading branch information
hanyujie2002 committed Oct 6, 2024
1 parent f4740f2 commit 519bf1a
Show file tree
Hide file tree
Showing 9 changed files with 75 additions and 53 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,6 @@ logs/

# Other common files to ignore
.DS_Store

/wyag.md
/wyag1.md
6 changes: 6 additions & 0 deletions .vitepress/config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ export default defineConfig({
markdown: {
config: (md) => {
md.use(footnote);
},
container: {
warningLabel: '警告',
noteLabel: '备注',
dangerLabel: '危险',
infoLabel: '信息',
}
},
themeConfig: {
Expand Down
28 changes: 15 additions & 13 deletions docs/1.-引言.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# 1. 引言

(2023 年 8 月)Wyag 现已完成!
> [!NOTE]
> (2023 年 8 月)Wyag 现已完成!
本文旨在从基础开始,深入解释 [Git 版本控制系统](https://git-scm.com/)。这听起来并不简单,过去的尝试往往效果不佳。但有一个简单的方法:要理解 Git 的内部机制,只需从头实现一个 Git。

Expand Down Expand Up @@ -36,15 +37,16 @@
- 在编程语言方面,wyag 将使用 **Python** 实现。代码将保持简单易懂,因此对于初学者来说,Python 看起来像伪代码,容易上手(讽刺的是,最复杂的部分将是命令行参数解析逻辑,但你不需要深入理解这个)。如果你会编程但从未接触过 Python,建议找个速成课程熟悉一下这门语言。
- `wyag``git` 都是终端程序。我假设你对 Unix 终端操作非常熟悉。再强调一遍,你不需要是个黑客,但 `cd``ls``rm``tree` 等命令应该是你工具箱里的基本工具。

**对 Windows 用户的说明**

`wyag` 应该能够在任何带有 Python 解释器的类 Unix 系统上运行,但我不确定它在 Windows 上的表现。测试套件绝对需要一个兼容 bash 的 shell,我相信 WSL 可以满足这一需求。此外,如果你使用 WSL,请确保你的 `wyag` 文件采用 Unix 风格的行结束符([请参见这个 StackOverflow 解决方案,适用于 VS Code](https://stackoverflow.com/questions/48692741/how-can-i-make-all-line-endings-eols-in-all-files-in-visual-studio-code-unix))。欢迎 Windows 用户提供反馈!

**致谢**

本文得益于多位贡献者的重要帮助,我对此深表感谢。特别感谢:

- GitHub 用户 [tammoippen](https://github.com/tammoippen),他草拟了我一度遗忘的 `tag_create` 函数(这是 [\#9](https://github.com/thblt/write-yourself-a-git/issues/9))。
- GitHub 用户 [hjlarry](https://github.com/hjlarry)[\#22](https://github.com/thblt/write-yourself-a-git/pull/22) 中修复了多个问题。
- GitHub 用户 [cutebbb](https://github.com/cutebbb)[\#27](https://github.com/thblt/write-yourself-a-git/pull/27/) 中实现了 `ls-files` 的第一个版本,从而让 wyag 实现了暂存区!

> [!WARNING]
> **对 Windows 用户的说明**
>
> `wyag` 应该能够在任何带有 Python 解释器的类 Unix 系统上运行,但我不确定它在 Windows 上的表现。测试套件绝对需要一个兼容 bash 的 shell,我相信 WSL 可以满足这一需求。此外,如果你使用 WSL,请确保你的 `wyag` 文件采用 Unix 风格的行结束符([请参见这个 StackOverflow 解决方案,适用于 VS Code](https://stackoverflow.com/questions/48692741/how-can-i-make-all-line-endings-eols-in-all-files-in-visual-studio-code-unix))。欢迎 Windows 用户提供反馈!
> [!NOTE]
> **致谢**
>
> 本文得益于多位贡献者的重要帮助,我对此深表感谢。特别感谢:
>
> - GitHub 用户 [tammoippen](https://github.com/tammoippen),他草拟了我一度遗忘的 `tag_create` 函数(这是 [\#9](https://github.com/thblt/write-yourself-a-git/issues/9))。
> - GitHub 用户 [hjlarry](https://github.com/hjlarry)[\#22](https://github.com/thblt/write-yourself-a-git/pull/22) 中修复了多个问题。
> - GitHub 用户 [cutebbb](https://github.com/cutebbb)[\#27](https://github.com/thblt/write-yourself-a-git/pull/27/) 中实现了 `ls-files` 的第一个版本,从而让 wyag 实现了暂存区!
18 changes: 10 additions & 8 deletions docs/4.-读取和写入对象hash-object-和-cat-file.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,21 @@

那么,**Git 对象到底是什么?** 从本质上讲,Git 是一个“基于内容寻址的文件系统”。这意味着,与普通文件系统不同,普通文件系统中,文件的名称是任意的,与文件内容无关,而 Git 存储的文件名称是根据其内容数学推导而来的。这有一个非常重要的含义:如果某个文本文件的单个字节发生变化,它的内部名称也会随之改变。简单来说:你在 Git 中并不是 *修改* 文件,而是在不同的位置创建一个新文件。对象就是这样:**在 Git 仓库中的文件,其路径由其内容决定**

**Git 其实并不是一个真正的键值存储**

一些文档,包括优秀的 [Pro Git](https://git-scm.com/book/id/v2/Git-Internals-Git-Objects),将 Git 称为“键值存储”。这并不错误,但可能会误导人。普通的文件系统实际上更接近于键值存储,而不是 Git。由于 Git 是从数据计算键的,因此可以更准确地称其为 *值值存储*
> [!WARNING]
> **Git 其实并不是一个真正的键值存储**
>
> 一些文档,包括优秀的 [Pro Git](https://git-scm.com/book/id/v2/Git-Internals-Git-Objects),将 Git 称为“键值存储”。这并不错误,但可能会误导人。普通的文件系统实际上更接近于键值存储,而不是 Git。由于 Git 是从数据计算键的,因此可以更准确地称其为 *值值存储*
Git 使用对象来存储很多东西:首先也是最重要的,就是它在版本控制中保存的实际文件——例如源代码。提交(commit)也是对象,标签(tag)也是。除了少数显著的例外(稍后会看到!),几乎所有东西在 Git 中都以对象的形式存储。

Git 存储给定对象的路径是通过计算其内容的 [SHA-1](https://en.wikipedia.org/wiki/SHA-1) [哈希值](https://en.wikipedia.org/wiki/Cryptographic_hash_function) 来确定的。更确切地说,Git 将哈希值表示为小写的十六进制字符串,并将其分为两部分:前两位字符和其余部分。它使用前两位作为目录名,其余部分作为文件名(这是因为大多数文件系统不喜欢在单个目录中有太多文件,这会导致性能下降。Git 的方法创建了 256 个可能的中间目录,从而将每个目录的平均文件数减少到 256 分之一)。

**什么是哈希函数?**

SHA-1 被称为“哈希函数”。简单来说,哈希函数是一种单向数学函数:计算一个值的哈希值很简单,但无法反向计算出哪个值生成了该哈希。

哈希函数的一个非常简单的例子是经典的 `len`(或 `strlen`)函数,它返回字符串的长度。计算字符串的长度非常容易,而且给定字符串的长度永远不会改变(当然,除非字符串本身发生变化!),但仅凭长度是不可能恢复原始字符串的。*密码学*哈希函数是同类函数的复杂版本,增加了一个特性:计算出一个输入值以生成给定的哈希是相当困难的,几乎不可能。(要生成一个长度为 12 的输入 `i`,你只需输入 12 个随机字符。使用如 SHA-1 这样的算法,则需要更长的时间——长到几乎不可能的程度[^1])。
> [!NOTE]
> **什么是哈希函数?**
>
> SHA-1 被称为“哈希函数”。简单来说,哈希函数是一种单向数学函数:计算一个值的哈希值很简单,但无法反向计算出哪个值生成了该哈希。
>
> 哈希函数的一个非常简单的例子是经典的 `len`(或 `strlen`)函数,它返回字符串的长度。计算字符串的长度非常容易,而且给定字符串的长度永远不会改变(当然,除非字符串本身发生变化!),但仅凭长度是不可能恢复原始字符串的。*密码学*哈希函数是同类函数的复杂版本,增加了一个特性:计算出一个输入值以生成给定的哈希是相当困难的,几乎不可能。(要生成一个长度为 12 的输入 `i`,你只需输入 12 个随机字符。使用如 SHA-1 这样的算法,则需要更长的时间——长到几乎不可能的程度[^1])。
在我们开始实现对象存储系统之前,必须了解它们的确切存储格式。一个对象以一个头部开始,头部指定其类型:`blob``commit``tag``tree`(稍后会详细介绍)。这个头部后面跟着一个 ASCII 空格(0x20),然后是以 ASCII 数字表示的对象大小(以字节为单位),接着是一个空字节(0x00),最后是对象的内容。在 Wyag 的仓库中,一个提交对象的前 48 个字节如下所示:

Expand Down
25 changes: 13 additions & 12 deletions docs/5.-阅读提交历史日志.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,14 +96,15 @@ def kvlm_parse(raw, start=0, dct=None):
return kvlm_parse(raw, start=end+1, dct=dct)
```

**对象身份规则**

我们使用 `OrderedDict`(一个有序的字典/哈希表)来确保字段总是以相同的顺序出现。这很重要,因为 Git 有**两个关于对象身份的强规则**

1. 第一个规则是 **相同的名称将始终引用相同的对象**。我们已经见过这个规则,它只是对象名称是其内容哈希值的结果。
2. 第二个规则则略有不同:**相同的对象将始终通过相同的名称引用**。这意味着不应该有两个等价的对象使用不同的名称。这就是字段顺序重要的原因:通过修改给定提交中字段出现的*顺序*,例如将 `tree` 放在 `parent` 后面,我们会修改提交的 SHA-1 哈希,从而创建两个等价但数值不同的提交对象。

例如,在比较树时,Git 会假设具有不同名称的两棵树**不同的——这就是为什么我们必须确保树对象的元素正确排序,以免生成不同但等价的树。
> [!NOTE]
> **对象身份规则**
>
> 我们使用 `OrderedDict`(一个有序的字典/哈希表)来确保字段总是以相同的顺序出现。这很重要,因为 Git 有**两个关于对象身份的强规则**
>
> 1. 第一个规则是 **相同的名称将始终引用相同的对象**。我们已经见过这个规则,它只是对象名称是其内容哈希值的结果。
> 2. 第二个规则则略有不同:**相同的对象将始终通过相同的名称引用**。这意味着不应该有两个等价的对象使用不同的名称。这就是字段顺序重要的原因:通过修改给定提交中字段出现的*顺序*,例如将 `tree` 放在 `parent` 后面,我们会修改提交的 SHA-1 哈希,从而创建两个等价但数值不同的提交对象。
>
> 例如,在比较树时,Git 会假设具有不同名称的两棵树**不同的——这就是为什么我们必须确保树对象的元素正确排序,以免生成不同但等价的树。
我们还需要编写类似的对象,因此让我们向工具箱中添加一个 `kvlm_serialize()` 函数。这非常简单:我们首先输出所有字段,然后是一行换行,接着是消息,最后再加一个换行。

Expand Down Expand Up @@ -226,7 +227,7 @@ dot -O -Tpdf log.dot

所有这些共同哈希成一个唯一的 SHA-1 标识符。

**等等,这是不是意味着 Git 是区块链?**

由于加密货币的缘故,区块链如今备受关注。是的,*在某种程度上*,Git 是一种区块链:它是一个通过加密手段连接在一起的块(提交)序列,保证每个元素都与结构的整个历史相关联。不过,不要太认真地看待这个比较:我们不需要 GitCoin。真的,我们不需要。

> [!NOTE]
> **等等,这是不是意味着 Git 是区块链?**
>
> 由于加密货币的缘故,区块链如今备受关注。是的,*在某种程度上*,Git 是一种区块链:它是一个通过加密手段连接在一起的块(提交)序列,保证每个元素都与结构的整个历史相关联。不过,不要太认真地看待这个比较:我们不需要 GitCoin。真的,我们不需要。
7 changes: 4 additions & 3 deletions docs/6.-读取提交数据检出.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,10 @@

模式只是文件的 [模式](https://en.wikipedia.org/wiki/File_system_permissions),路径是它的位置。SHA-1 可能指向一个 blob 或另一个树对象。如果是 blob,路径就是文件;如果是树,则是目录。为了在文件系统中实例化这个树,我们将首先加载与第一个路径(`.gitignore`)相关联的对象,并检查它的类型。由于它是一个 blob,我们将创建一个名为 `.gitignore` 的文件,内容为这个 blob 的内容;对 `LICENSE``README.md` 也是如此。但与 `src` 相关联的对象不是一个 blob,而是另一个树:我们将创建目录 `src`,并在该目录中用新的树重复相同的操作。

**路径是单一的文件系统条目**

路径精确地标识一个文件或目录。不是两个,也不是三个。如果你有五层嵌套的目录,即使四个目录是空的,只有下一个目录有内容,你也需要五个树对象递归地相互引用。你不能通过将完整路径放在单个树条目中来走捷径,例如 `dir1/dir2/dir3/dir4/dir5`
> [!WARNING]
> **路径是单一的文件系统条目**
>
> 路径精确地标识一个文件或目录。不是两个,也不是三个。如果你有五层嵌套的目录,即使四个目录是空的,只有下一个目录有内容,你也需要五个树对象递归地相互引用。你不能通过将完整路径放在单个树条目中来走捷径,例如 `dir1/dir2/dir3/dir4/dir5`
## 6.2. 解析树对象

Expand Down
24 changes: 14 additions & 10 deletions docs/7.-引用标签和分支.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@ Git 引用,简称 refs,可能是 Git 中保存的最简单类型的对象。
ref: refs/remotes/origin/master
```

**直接引用和间接引用**

从现在开始,我将把形如 `ref: path/to/other/ref` 的引用称为**间接引用**,而带有 SHA-1 对象 ID 的引用称为**直接引用**
> [!NOTE]
> **直接引用和间接引用**
>
> 从现在开始,我将把形如 `ref: path/to/other/ref` 的引用称为**间接引用**,而带有 SHA-1 对象 ID 的引用称为**直接引用**
本节将描述引用的用途。现在,重要的是以下几点:

Expand Down Expand Up @@ -103,7 +104,8 @@ git checkout v12.78.52
git checkout 6071c08
```

版本是标签的一个常见用途,但就像 Git 中几乎所有事物一样,标签没有预定义的语义:它们可以根据你的需求而定,并可以指向任何你想要的对象,甚至可以给 *blob* 打标签!
> [!NOTE]
> 版本是标签的一个常见用途,但就像 Git 中几乎所有事物一样,标签没有预定义的语义:它们可以根据你的需求而定,并可以指向任何你想要的对象,甚至可以给 *blob* 打标签!
## 7.3. 轻量标签和标签对象,以及解析标签对象

Expand Down Expand Up @@ -225,9 +227,10 @@ def ref_create(repo, ref_name, sha):

那么 **当前** 分支呢?实际上更简单。它是位于 `refs` 层级之外的一个引用文件,位于 `.git/HEAD`,这是一个 **间接** 引用(即,它的形式是 `ref: path/to/other/ref`,而不是简单的哈希)。

**分离的 HEAD**

当你检出一个随机提交时,Git 会警告你处于“分离的 HEAD 状态”。这意味着你不再处于任何分支中。在这种情况下,`.git/HEAD` 是一个 **直接** 引用:它包含一个 SHA-1。
> [!NOTE]
> **分离的 HEAD**
>
> 当你检出一个随机提交时,Git 会警告你处于“分离的 HEAD 状态”。这意味着你不再处于任何分支中。在这种情况下,`.git/HEAD` 是一个 **直接** 引用:它包含一个 SHA-1。
## 7.6. 引用对象:`object_find` 函数

Expand All @@ -242,9 +245,10 @@ def ref_create(repo, ref_name, sha):

请注意最后两步是如何 *收集* 值的:前两步是绝对引用,因此我们可以安全地返回结果。但短哈希或分支名称可能是模糊的,我们希望枚举名称的所有可能含义,并在找到多个结果时抛出错误。

**短哈希**

为了方便,Git 允许通过名称的前缀来引用哈希。例如,`5bd254aa973646fa16f66d702a5826ea14a3eb45` 可以被称为 `5bd254`。这被称为“短哈希”。
> [!INFO]
> **短哈希**
>
> 为了方便,Git 允许通过名称的前缀来引用哈希。例如,`5bd254aa973646fa16f66d702a5826ea14a3eb45` 可以被称为 `5bd254`。这被称为“短哈希”。
```python
def object_resolve(repo, name):
Expand Down
14 changes: 8 additions & 6 deletions docs/8.-处理暂存区和索引文件.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,10 @@

3. 当你将这些更改 `git commit` 时,将从索引文件生成一个新的树对象,生成一个新的提交对象,并更新分支,然后完成。

**关于术语的说明**

因此,暂存区和索引是同一个概念,但“暂存区”这个名称更像是 Git 用户可见的功能名称(可以用其他方式实现),是某种抽象;而“索引文件”则专指这一抽象功能在 Git 中的实际实现方式。
> [!NOTE]
> **关于术语的说明**
>
> 因此,暂存区和索引是同一个概念,但“暂存区”这个名称更像是 Git 用户可见的功能名称(可以用其他方式实现),是某种抽象;而“索引文件”则专指这一抽象功能在 Git 中的实际实现方式。
## 8.2. 解析索引

Expand Down Expand Up @@ -417,9 +418,10 @@ hello.html
wyag.zip
```

**这只是一个近似实现**

这并不是一个完美的重新实现。特别是,通过仅使用目录名称的规则(例如 `__pycache__`)来排除整个目录将不起作用,因为 `fnmatch` 需要模式为 `__pycache__/**`。如果你真的想玩弄忽略规则,[这可能是一个不错的起点](https://github.com/mherrmann/gitignore_parser)
> [!WARNING]
> **这只是一个近似实现**
>
> 这并不是一个完美的重新实现。特别是,通过仅使用目录名称的规则(例如 `__pycache__`)来排除整个目录将不起作用,因为 `fnmatch` 需要模式为 `__pycache__/**`。如果你真的想玩弄忽略规则,[这可能是一个不错的起点](https://github.com/mherrmann/gitignore_parser)
## 8.5. status 命令

Expand Down
3 changes: 2 additions & 1 deletion docs/9.-暂存区和索引第二部分暂存和提交.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,8 @@ def index_write(repo, index):

对索引进行的最简单修改是从中移除一个条目,这意味着下一个提交**将不包括**该文件。这就是 `git rm` 命令的作用。

`git rm`**破坏性的**`wyag rm` 也是如此。该命令不仅修改索引,还会从工作区中删除文件。与 git 不同,`wyag rm` 不关心它移除的文件是否已保存。请谨慎操作。
> [!DANGER]
> `git rm`**破坏性的**`wyag rm` 也是如此。该命令不仅修改索引,还会从工作区中删除文件。与 git 不同,`wyag rm` 不关心它移除的文件是否已保存。请谨慎操作。
`rm` 接受一个参数,即要移除的路径列表:

Expand Down

0 comments on commit 519bf1a

Please sign in to comment.