# Version Control

Authors: Fanyi Pu, GPT-6 Astra

Published: 2026-10-18

Canonical: <https://pufanyi.com/blog/cs/notes/version-control-design>

从日常 Git 操作出发，探索不同版本控制系统的数据模型、协作方式与客户端和服务端设计，以及大规模文件和 Agent 并发工作的管理。

## 从一次熟悉的修改开始

假设这篇博客的正文保存在 `article.md`，已经用 Git 保存过一个版本。写到一半，我们发现开头只列了几条命令，没有解释每一步的作用，于是补上一段说明。

检查过修改，确认这段解释值得保留，就像平时一样提交它：

```bash
git add article.md
git commit -m "Explain the opening example"
```

过了一会儿，重读文章时又觉得旧版更简洁，想把两个版本放在一起比较。编辑器里已经是修改后的内容，但 Git 仍然能找回修改前的整篇文章。

上一次提交中的 article.md

原来的开头，后面的正文没有修改。可以分别查看两次提交中的完整文章。

上一次提交

article.md

原来的开头

[View diagram in the original article](https://pufanyi.com/blog/cs/notes/version-control-design)

这一次提交中的 article.md

开头补上解释，后面的正文没有修改。可以分别查看两次提交中的完整文章。

这一次提交

article.md

开头补上解释

[View diagram in the original article](https://pufanyi.com/blog/cs/notes/version-control-design)

改动集中在开头，但两次提交都能让我们查看当时的整篇文章。图中展示的是可恢复的内容，并不表示磁盘上存了两份完整副本。

这件事很熟悉，却值得追问：**我们只改了一段，Git 为了找回旧版，究竟保存了什么？**

一种容易想到的办法是，每次提交都把整篇文章复制一份。这样找回旧版很直接，但如果文章很长、修改很频繁，大量相同内容就会被反复保存。另一种办法是，保留最初的文章，再依次记录每次删掉和增加了什么；这样看起来少存了一些内容，但恢复某个版本时就需要把相应的修改还原出来。

我们平时看到的 `git diff` 很像第二种办法：删掉的行和增加的行一目了然。不过，**能够展示两版之间的差异，并不能说明底层就是按这份差异保存的**。两篇完整的文章也可以拿来比较，得到同样的 diff。

而一个博客仓库通常还包括其他文章、图片和样式。这次虽然只改了 `article.md`，我们仍然希望能够找回当时整个仓库中由 Git 跟踪的文件：哪篇文章配哪张图，用的是哪个版本的样式。没有改动的文件也必须能从那个版本中找到。

所以，接下来要弄清楚两件事：一个提交怎样确定当时有哪些文件、各自是什么内容；这些版本在磁盘上又怎样保存，才能避免不必要的重复。先从 Git 如何通过一个提交找到一份文件开始。

## 一次提交究竟记录了什么

要找回旧版文章，至少需要知道两件事：当时的文件叫 `article.md`，以及它当时的内容。如果还要恢复整个博客，就要对每个文件保留这样的对应关系。

先把仓库缩小到两个文件：`article.md` 是正文，`style.css` 决定页面样式。这次只改正文，样式不动。我们希望新版本能找到新正文和原来的样式，旧版本则仍然能找到旧正文和同一份样式。

### 把内容和文件名分开

一个直接的做法是，把文件内容单独保存，再用一份目录清单记录“这个文件名对应哪份内容”。这样，两个版本都需要同一份样式时，两份清单可以指向同一份内容，不必各存一份。

Git 用 **blob** 保存文件内容，用 **tree** 保存目录条目。blob 本身不记录文件名；tree 中的条目把名字、文件模式和对象 ID 联系起来。如果条目是子目录，它指向另一个 tree，逐层展开就能得到完整路径。这是 [Git 对象模型](https://git-scm.com/book/en/v2/Git-Internals-Git-Objects)中内容与目录的分工。

这里的对象 ID，可以先理解为用来取回对象的地址。它由对象类型、长度和内容一起计算哈希得到；在同一种对象格式下，相同的内容会得到相同的 blob ID。因此，文件即使改了名字，只要存入 Git 的内容没有改变，就可以继续使用原来的 blob。具体的编码与哈希范围见 [Git 对象格式](https://git-scm.com/docs/gitformat-loose)。

我们可以直接查看前后两次提交的目录清单。下面的 `HEAD` 表示当前提交，`HEAD~1` 表示它的第一个父提交；在这个只有两次顺序提交的例子里，就是修改前的版本。

```bash
git ls-tree --abbrev=7 HEAD~1
git ls-tree --abbrev=7 HEAD
```

[`git ls-tree`](https://git-scm.com/docs/git-ls-tree) 列出指定版本中的目录条目。这个小仓库里，修改前的输出是：

```text
100644 blob 156b3ea article.md
100644 blob 7338e12 style.css
```

修改后则是：

```text
100644 blob 876a559 article.md
100644 blob 7338e12 style.css
```

从左到右分别是文件模式、对象类型、缩短后的对象 ID 和文件名。`100644` 表示普通的非可执行文件。这里最值得看的是两组 ID：正文由 `156b3ea` 变成了 `876a559`，样式始终是 `7338e12`。这些 ID 来自本节的实际小实验，复现步骤放在后面的折叠框里。

新 tree 仍然列出两个文件，只是正文条目改为指向新 blob。旧 tree 和旧 blob 也仍然存在，所以从旧版本出发，依然能找到修改前的正文。**每个版本都描述完整的文件状态，但不同版本可以共享没有变化的对象。**

### 再给这份目录状态加上历史

目录和内容解决了“这个版本有哪些文件”，却还没有告诉我们是谁保存了这个版本、为什么保存，以及它接在哪个版本之后。

Git 用 **commit 对象**记录这些信息：它指向一个根 tree，并记录父提交、作者、提交者、时间和提交说明。第一次提交没有父提交；本例中的第二次提交有一个父提交；合并提交则可以有多个父提交。字段的含义可以对照 [`git commit-tree` 文档](https://git-scm.com/docs/git-commit-tree)。

两次提交共享未变化的样式内容

每个 commit 指向自己的根 tree。旧 tree 的 article.md 指向旧正文 blob，新 tree 的 article.md 指向新正文 blob；两棵 tree 的 style.css 都指向同一个样式 blob。新 commit 另有一条 parent 引用指向旧 commit。实线表示查找文件状态，虚线表示历史关系。

上一次 commit

tree

旧 tree

article.md

style.css

旧正文 blob

这一次 commit

新 tree

新正文 blob

parent

同一份样式 blob

7338e12

[View diagram in the original article](https://pufanyi.com/blog/cs/notes/version-control-design)

每条实线都表示对象引用：正文变了，新 tree 改为引用新正文；样式没变，两棵 tree 共用同一个 blob。虚线 parent 连接提交历史，与查找文件的路径分开。

图中有两种关系。沿着 `tree` 方向走，可以找到这个版本的文件；沿着 `parent` 方向走，可以找到之前的提交。恢复当前版本的文件状态，并不需要沿着 parent 把每次文字修改重新做一遍。

用下面的命令，可以把当前 commit 对象的内容直接读出来：

```bash
git cat-file -p HEAD
```

输出结构如下。为了突出字段，这里用占位名称代替完整 ID、身份和时间：

```text
tree <根 tree 的 ID>
parent <上一次 commit 的 ID>
author <作者及时间>
committer <提交者及时间>

Explain the opening example
```

commit 自己也有对象 ID。因为父提交、时间和提交说明都是它的内容，所以即使文件完全一样，这些信息不同，也可以得到不同的 commit。**“项目内容相同”和“同一个提交”是两回事。**

现在，找回旧版文章的路径就清楚了：先找到旧 commit，再找到它的根 tree，从中查出 `article.md` 对应的 blob，最后读出正文。Git 把这几步包装成了一个命令：

```bash
git show HEAD~1:article.md
```

它只显示旧内容，不会覆盖正在编辑的文件。若要比较前后两版，则可以执行：

```bash
git diff HEAD~1 HEAD -- article.md
```

这次 diff 是从两个版本的内容计算出来的，commit 对象里并没有保存这份逐行差异。

复现这个两文件实验

在一个新目录中运行下面的命令。需要已经配置 Git 的提交姓名和邮箱。例子没有设置内容转换规则，两个文件都作为普通文本保存。

```bash
mkdir blog-snapshot-example
cd blog-snapshot-example
git init

cat > article.md <<'ARTICLE'
# Version Control

修改文章后，提交保存。
ARTICLE

cat > style.css <<'STYLE'
body { line-height: 1.8; }
STYLE

git add article.md style.css
git commit -m "Write the opening"

cat > article.md <<'ARTICLE'
# Version Control

修改文章后，提交保存，之后可以比较和找回旧版。
ARTICLE

git add article.md
git commit -m "Explain the opening example"
```

然后执行正文中的 `ls-tree`、`cat-file`、`show` 和 `diff` 命令。本节展示的缩写来自 SHA-1 格式的仓库；使用 SHA-256 的仓库会得到不同 ID。文件的空格、换行和编码也会影响 blob ID，提交的身份和时间则会影响 commit ID。观察的重点是：文章内容变了，样式内容没变，它们的对象 ID 是否相应变化。

### 暂存区决定下一份目录状态

知道了 tree 的作用，再回头看 `git add` 和 `git commit` 就容易理解了：创建新提交之前，需要先确定新 tree 中各个路径应该对应哪些内容。

假设正文已经改好，但我们还在试验另一种页面样式，暂时不想提交样式修改。此时只执行 `git add article.md`，然后普通地执行 `git commit`，得到的就应当是“新正文 + 上一次提交的样式”，而不是整个工作目录此刻的样子。

**暂存区（index）就是用来准备这份待提交状态的。** 在本例没有合并冲突的情况下，它记录路径、文件模式和选定的 blob ID 等信息。未修改的已跟踪文件也有条目，因此它不只是一张“这次改过哪些文件”的清单。执行 `git add article.md` 会保存选中的正文内容，并更新对应条目；[此后继续编辑正文，需要再次暂存](https://git-scm.com/docs/git-add)，新的编辑才会进入下一次普通提交。

可以用 [`git ls-files --stage`](https://git-scm.com/docs/git-ls-files) 观察暂存区中的条目：

```bash
git ls-files --stage
```

`git commit` 据此生成目录树，再创建指向这棵树的 commit。于是，工作目录是正在编辑的文件，index 是选好的待提交状态，commit 则是已经记录下来的版本。三者可以不同，因为“正在尝试什么”与“这次决定保存什么”本来就可以不同。

提交之后，我们通常也不必记住它的长 ID。[分支引用](https://git-scm.com/book/en/v2/Git-Internals-Git-References)把 `main` 这样的名字关联到一个 commit；在通常附着于分支的状态下，`HEAD` 指向当前分支。创建新提交后，当前分支指向它，旧提交仍然通过 parent 连接在历史中。分支的分叉与合并，后面再展开。

### 完整快照不等于每次完整复制

现在可以区分三个层次：

| 层次   | 回答的问题         | 本例中的表现             |
| ---- | ------------- | ------------------ |
| 版本模型 | 这个提交对应什么文件状态？ | 根 tree 能找到正文和样式的内容 |
| 差异展示 | 两个版本之间改了什么？   | diff 显示正文中新增的解释    |
| 物理存储 | 这些对象怎样占用磁盘空间？ | 相同对象复用，不同对象还可以压缩存储 |

共享 blob 已经避免了反复保存未变化的样式，但正文只改了一句话，新旧 blob 之间也有大量相同内容。Git 还可以在 [packfile](https://git-scm.com/book/en/v2/Git-Internals-Packfiles) 中把相似对象做 delta 压缩：保存一个基准对象，以及重建另一个对象所需的差异。这样的存储依赖不要求与 commit 的父子关系一致，基准也不必是历史上更早的版本。

因此，读取一个 blob 时，底层可能需要解压和重建；但重建后，它仍然是那个 ID 所标识的完整对象。压缩改变的是对象怎样存放，不改变 commit、tree 和 blob 之间的关系。具体的基准引用与重建指令见 [pack 格式](https://git-scm.com/docs/gitformat-pack)。

回到“找回旧版文章”：commit 确定一个版本，tree 确定路径与内容的对应，blob 提供内容；parent 则把版本连成历史。这个分工让 Git 能描述完整快照，同时共享和压缩内容。后面比较其他系统时，也需要分别问清楚它们怎样表示一个版本，以及怎样实际保存这些版本。

## 版本控制有哪些不同的设计方向

- 先区分数据模型、工作流、协作平台、存储与传输、服务端架构；一个系统可以同时涉及多个层面。
- 对每条路线追问同一组问题：记录什么、如何标识变化、怎样组合修改、客户端保留什么、服务器负责什么，以及适用条件和代价。
- 调研覆盖下面所有路线；用具体机制和小实验展开，避免写成工具名单或从 SVN 到 Git 再到 jj 的单线升级史。

| 研究方向         | 系统与工具                                 | 要解释的问题                                   |
| ------------ | ------------------------------------- | ---------------------------------------- |
| 集中式版本管理      | SVN、Perforce                          | 全局版本、工作副本、原子提交、路径权限和文件锁怎样配合？             |
| 分布式版本管理      | Git、Mercurial                         | 本地历史、提交身份、分支、存储格式和同步有哪些不同选择？             |
| 基于 patch 的模型 | Darcs、Pijul                           | 变更的上下文、依赖、交换顺序和冲突解决怎样表示？                 |
| 本地修改与历史整理    | jj、Sapling                            | 怎样保存工作进展、追踪改写、调整提交堆栈和恢复操作？               |
| Git 上的协作工作流  | GitButler、Graphite                    | 并行修改与 stacked PR 怎样组织？哪些状态属于 Git，哪些属于工具？ |
| 集成式项目协作      | Fossil                                | 代码、issue、wiki 等信息如何保存、同步、部署与迁移？          |
| 文件与数据版本管理    | Git LFS、git-annex、DVC、Xet、Oxen、lakeFS | 文件身份、位置、分块、数据版本与代码版本如何关联？                |
| 大规模服务端与工作区   | Piper、Mononoke、EdenFS、Scion           | 查询、提交、缓存和按需获取如何分工，规模与可用性如何取舍？            |

## 如果历史主要保存在服务器上

### SVN：一个仓库版本与多个工作副本

- 从 SVN 的全局 revision 和原子提交出发，对照 Git 的本地提交与后续 push，解释“保存一个版本”在两者中的不同含义。
- 观察工作副本的基准、局部更新和 mixed revisions；区分仓库中的完整状态与本地文件的组合。
- 研究以目录复制表示 branch/tag 的方式、merge tracking、离线可完成的操作与必须访问服务器的操作。
- 小实验：两个工作副本分别修改、提交、更新与合并；记录每一步客户端和服务器的状态。

### Perforce：workspace、changelist 与文件协作

- 研究 depot 到 workspace 的映射，以及 pending/submitted changelist、shelve 和 streams 分别表达什么。
- 从难以自动合并的素材出发，比较并发编辑、提交锁与 exclusive checkout；区分 `p4 lock` 和 `+l` 的作用。
- 讨论路径权限、局部同步、断线工作与恢复；分析为什么文件锁仍然无法保证跨文件的语义一致性。
- 对照实验：两人修改同一二进制文件，以及分别修改相互依赖的两个文件，观察需要在哪里协调。

## 分支、合并和变基在改变什么

- 从共同版本出发，让两项修改形成分叉；解释提交图与分支引用。
- 从“只比较两个最终文件缺少什么信息”推导共同祖先与三方合并。
- 通过一个小例子观察 merge、rebase 和 amend 前后的提交图与身份变化。
- 区分文本冲突与行为上的不兼容：没有冲突标记，不代表组合后的程序正确。
- 解释撤销修改、移动引用与恢复历史的区别，以及 reflog 能帮助找回什么。

## 如果把可组合的变更放在中心

- 从“只拿走一项功能，却不带上无关修改”的需求出发，研究 Darcs 和 Pijul 的 patch 模型。
- 用两项独立修改和一项依赖修改，逐步解释上下文、依赖与 patch commutation；区分交换操作与任意重排。
- 分别研究 Darcs 的 patch algebra 与 Pijul 的图表示，不把它们合并成同一种实现。
- 观察冲突与冲突解决怎样被记录、传播和复用，比较 Git cherry-pick、rebase 与 patch 选择的语义。
- 区分这里的逻辑变更模型与文件压缩的 delta；理论上的合并性质不等于程序行为正确。
- 小实验：让两个副本以不同顺序引入独立修改，再加入依赖和冲突，记录结果与限制。

## 本地工作流还有哪些设计

### 用 jj 重新组织同一项工作

- 完成一个连续任务：实现功能、拆出无关修改、补充前面的提交、处理上游冲突、撤销一次错误操作。
- 每一步给出操作意图、必要命令和前后的状态图，再与 Git 的表达方式对照。
- 展开 working-copy commit、change ID 与 commit ID、bookmarks、后续修改的调整、冲突记录及 operation log。
- 解释 jj 的工作流模型与 Git 存储后端之间的关系；补充与现有 Git 工具协作的边界。
- 待实验时记录 jj 版本，核对命令与兼容性，不把两套操作方式简单逐条对应。

### Mercurial 与 Sapling：怎样追踪正在演化的修改

- 对照 Mercurial 的 changelog、manifest、filelog 与 revlog，分清逻辑版本模型和实际存储结构；研究 named branches、bookmarks 与 Git 分支的区别。
- 研究 Mercurial 的 phases、obsolescence markers 与 changeset evolution，区分已发布历史、可修改历史及改写关系；核对相关扩展和实验性功能的边界。
- 用同一项任务研究 Sapling 的 smartlog、提交堆栈、历史改写与恢复，并与 Git、jj 对照。
- 区分 Sapling CLI、Mononoke 服务端和 EdenFS 工作区各自的职责，分别核对可用功能与 Git 兼容模式。

### GitButler 与 Graphite：怎样组织并行修改与评审

- 用 GitButler 研究同一工作目录里的并行修改、改动归属与提交整理；检查共享目录并未隔离的运行状态。
- 用 Graphite 研究 stacked branches/PR、评审修改和 restack；与多个提交放进一个 PR 的方式对照。
- 区分提交依赖、任务依赖和评审依赖；观察工具元数据、普通 Git 操作与远端平台如何保持一致。
- 比较退出这些工具后能够保留的历史和仍需迁移的协作状态。

## 一次 clone、fetch 和 push 交换了什么

- 追踪本地与远端分别持有哪些对象和引用，以及同步时如何确定缺失的数据。
- 区分对象传输、引用更新和工作目录的生成；解释为什么拿到版本信息不等于已经展开所有文件。
- 比较完整克隆、shallow clone、partial clone 与 sparse checkout 各自减少了什么，以及保留了哪些成本。
- 贯穿讨论历史查询、diff 与 merge 等计算发生的位置，以及离线能力、网络请求和缓存的取舍。

## 文件数量和文件体积，是两种规模问题

### 大量小文件：时间花在哪里

- 分别考察目录遍历、变更检测、元数据索引、对象查找、网络请求和文件展开的成本。
- 从重复扫描与重复读取出发，讨论文件监听、缓存、批量请求、按需检出和虚拟文件系统。
- 设计对照实验：在总字节数接近时改变文件数量，分别观察 status、传输与 checkout；区分冷缓存和热缓存。

### 超大文件：能否只保存和传输变化的部分

- 从外置文件开始，推导指针、内容身份、数据上传与旧版本恢复的配合；在这里解释 Git LFS。
- 区分整文件去重、文件分块与内容定义分块，观察局部修改、插入和重新编码对复用的影响。
- 用 Xet 作为分块存储与传输的案例，核对客户端、服务端及托管环境的要求。
- 分开衡量存储量、上传量、下载量和重建成本，不用单一“更快”概括全部操作。

### 代码和数据怎样恢复到同一个实验状态

- 从“旧代码配上新数据无法复现”引出代码、数据、配置和产物之间的版本对应，在这里讨论 DVC。
- 讨论历史数据保留、可达性、垃圾回收与缺失对象；区分版本记录和备份。
- 用 git-annex 研究文件位置、按需获取、离线介质和副本保留策略。
- 用 Oxen 研究独立的数据仓库、数据差异、分支与客户端/服务端协作；用 lakeFS 研究对象存储上的版本元数据、分支与发布。
- 对照实验：固定代码与数据版本、恢复旧状态、在独立分支修改数据；检查版本一致性与实际复制的数据量。

## 协作信息是否也属于项目历史

- 从 Git 仓库迁移后 PR、issue 和评审记录可能仍留在原平台的问题出发，划分版本控制与托管平台的边界。
- 研究 Fossil 的 SQLite 存储、版本历史与内置 issue/wiki 等协作功能，观察这些信息如何同步和关联。
- 对照 Git 加独立托管平台的组合，分析备份、离线使用、权限、部署和迁移的取舍。
- 小实验：在另一个位置恢复一个项目，检查代码之外的讨论、文档与链接还保留了什么。

## 多个 Agent 怎样共同修改一个项目

### 一项任务需要怎样的工作环境

- 跟踪任务从起始版本、获取文件、执行修改到交付结果的过程，明确任务与提交的关系。
- 比较共享目录、独立 worktree 与独立运行环境分别隔离了什么。
- 检查依赖、生成文件、缓存、进程和外部服务是否仍被共享；区分历史隔离与执行隔离。
- 讨论按任务获取文件、复用只读内容，以及任务、修改和验证记录的关联。

### 多份结果怎样成为一个可信的新版本

- 构造两个 Agent 修改不同文件、各自测试通过、整合后却失败的例子。
- 解释验证针对的具体版本，以及检查期间主分支变化带来的问题；讨论整合顺序与重新验证。
- 区分保存工作进展、提交评审、接纳结果与部署；明确权限和人工检查的位置。
- 讨论中断、超时、重试、重复交付、放弃任务和回滚时，哪些状态需要保留或清理。

## 重新分配客户端与服务端的职责

- 汇总前面的操作：保存内容、创建版本、查询历史、计算 diff、合并、验证和检出。
- 在客户端始终在线、仓库巨大、任务频繁且只访问局部内容的条件下，重新讨论这些操作的位置。
- 对照 Google 公开的 Piper/CitC 架构、Meta 的 Mononoke/EdenFS，以及 Anthropic 的 Scion，分别研究版本存储、服务端查询、工作区与缓存。
- 追踪一次局部检出、一次历史查询和一次提交，画出各系统中客户端、缓存与权威存储之间的数据路径。
- 对照 SVN、Perforce 与这些大型仓库架构，分析共同的需求与不同的实现，避免把服务端设计简单归结为集中式或分布式。
- 对内部系统依据公开论文和工程文章讨论，标明资料年代与证据范围，不将架构案例视为可直接安装的产品。
- 分析集中服务带来的权限、可用性、故障恢复和运维成本，区分系统架构与用户操作体验。

## 回到项目：这些设计分别解决了什么

- 用一张表连接问题、机制、成本与适用条件，区分操作体验、文件管理和并发协作。
- 对照小型开发项目、数据密集项目与大量 Agent 任务，说明哪些约束变化会影响设计选择。
- 整理实验中验证的结论与尚未覆盖的边界，补齐各机制对应的官方资料和引用。

## 资料入口

以下作为后续展开与实验的起点，正文中的具体结论仍需逐项核对；工具操作记录实际版本，历史架构资料注明年代。

- [Pro Git：Git Internals](https://git-scm.com/book/en/v2/Git-Internals-Plumbing-and-Porcelain)。
- [Version Control with Subversion](https://svnbook.red-bean.com/)；选取与实验版本匹配的章节。
- [Mercurial：changeset evolution](https://www.mercurial-scm.org/help/topics/evolution)。
- [Perforce：workspaces and changelists](https://help.perforce.com/helix-core/server-apps/cmdref/current/Content/P4Guide/overview.files.workspaces-and-changelists.html) 与 [exclusive checkout](https://help.perforce.com/helix-core/server-apps/cmdref/current/Content/P4Guide/resolve.lock.exclusive.html)。
- [Darcs：patch theory](https://darcs.net/Theory) 与 [Pijul：theory](https://pijul.org/manual/theory.html)。
- [jj：与 Git 的模型对照](https://docs.jj-vcs.dev/latest/git-comparison/) 与 [Sapling 文档](https://sapling-scm.com/docs/introduction/)。
- [GitButler：parallel branches](https://docs.gitbutler.com/features/branch-management/virtual-branches) 与 [Graphite：stack 工作流](https://graphite.com/docs/cli-quick-start)。
- [Fossil 与 Git 的对照](https://fossil-scm.org/home/doc/trunk/www/fossil-v-git.wiki)；其中的评价需结合双方文档与实验判断。
- [DVC：缓存结构](https://doc.dvc.org/user-guide/project-structure/internal-files)、[Xet：存储后端](https://huggingface.co/docs/hub/xet/index)、[git-annex：副本管理](https://git-annex.branchable.com/copies/)、[Oxen](https://github.com/Oxen-AI/Oxen) 与 [lakeFS：数据版本](https://docs.lakefs.io/guides/version-data/)。
- [Google：Why Google Stores Billions of Lines of Code in a Single Repository（2016）](https://research.google/pubs/why-google-stores-billions-of-lines-of-code-in-a-single-repository/)、[Mononoke 架构](https://github.com/facebook/sapling/blob/main/eden/mononoke/docs/1.3-architecture-overview.md) 与 [Anthropic：Redesigning source control（2026）](https://www.anthropic.com/engineering/redesigning-source-control)。
