Wasserpuncher/nanoclone

GitHub: Wasserpuncher/nanoclone

仅用 Python 标准库从零实现的 git clone,覆盖 Git Protocol v2 通信、packfile 解析与 delta 解析的完整流程,旨在揭示版本控制工具背后的底层协议机制。

Stars: 0 | Forks: 0

# nanoclone **从头开始编写的 `git clone`。零依赖——甚至不需要 `git`。** nanoclone 直接使用 Git 的通信协议:它通过 Smart HTTP v2 进行协商, 下载 packfile,解析其中的 delta 链,根据 SHA-1 校验每一个 object, 并写入工作树以及有效的 `.git` 索引。 最终得到的仓库**被真正的 Git 视为干净状态**: ``` $ python -m nanoclone clone https://github.com/psf/requests Cloning into 'requests'... branch main -> 4c800e9aea20 received pack: 13232.5 KiB in 2.8s resolved 25624 objects checked out 130 files $ cd requests && git status --porcelain # no output: Git sees nothing to do $ git fsck --strict # the object store is consistent ``` 这里没有任何地方去调用 `git`。仅导入了 `hashlib`、`zlib`、 `struct`、`urllib`、`os`、`pathlib`、`argparse`、`sys` 和 `time`。 ## 为什么会有这个项目 Git 通常是一个黑盒。克隆一个仓库看起来只是一条命令,但 在其底层是四种堆叠在一起的不同格式,而且 它们中的每一个都简单到足以在一个下午内读完: | 层级 | 它是什么 | | --- | --- | | **pkt-line** | 每条消息前面的 4 字节十六进制长度前缀。其中三个长度被保留作为控制数据包。 | | **Protocol v2** | 基于 HTTP 的请求/响应 RPC。`ls-refs` 查询现有内容,`fetch` 请求 object。 | | **Packfile** | 包含每个 object,并经过 zlib 压缩。大多数存储为相对于另一个 object 的 *delta*。 | | **Object 模型** | commit 指向 tree,tree 指向 blob。每个 object 的名称是其内容的 SHA-1 哈希值。 | 最后一个特性使得这个项目是可验证的,而不仅仅是看起来 可行:如果我们解析时错了一个 bit,哈希值就会改变, 并且我们计算出的 object id 就会停止与 Git 计算出的相匹配。在这里, 正确性不是凭主观判断的。 ## 安装 要求 Python 3.10 或更高版本。无需安装: ``` $ git clone https://github.com/Wasserpuncher/nanoclone && cd nanoclone $ python -m nanoclone --help ``` ## 用法 ``` # Clone 远程的默认分支 $ python -m nanoclone clone https://github.com/octocat/Hello-World # 选择一个分支和目标位置 $ python -m nanoclone clone https://github.com/octocat/Hello-World hi -b test # 列出远程通告的 refs,而不下载任何内容 $ python -m nanoclone ls-remote https://github.com/octocat/Hello-World 7fd1a60b01f91b314f59955a4e4d4e80d8edf11d HEAD -> refs/heads/master 7fd1a60b01f91b314f59955a4e4d4e80d8edf11d refs/heads/master # 拆解 packfile:类型、大小、偏移量,以及每个 delta 修补的是哪个 object $ python -m nanoclone verify-pack .git/objects/pack/pack-*.pack ``` ## clone 实际上是如何工作的 1. **发现。** 发送带有请求头 `Git-Protocol: version=2` 的 `GET /info/refs?service=git-upload-pack`。服务器会响应其能力。 2. **`ls-refs`。** 发送一个 `POST` 询问存在哪些 ref。回复会告诉我们 每个分支的 commit id,并且——通过 `symref-target`——指出 `HEAD` 指向哪个分支。 3. **`fetch`。** 发送一个 `POST` 说明 `want ` 和 `done`。直接发送 `done` 会跳过协商,因此服务器会回复一个完整的 packfile。它的字节多路复用在 sideband 通道 1 上传输,进度信息在 通道 2 上,而错误在通道 3 上。 4. **解包。** packfile 的结构是 `PACK`、一个版本号、一个 object 数量,接着是 对应数量的压缩 object,最后是对其前面所有内容的 SHA-1 校验和。 5. **解析 delta。** 七种 object 类型中有两种根本不是 object,而是 补丁:`OFS_DELTA` 通过向后偏移量来命名其 base,而 `REF_DELTA` 通过 base 的 SHA-1 来命名。delta 是一个微型程序,包含 *从 base 复制此范围* 和 *插入这些字面字节*。因为 `REF_DELTA` 可能指向存储在 pack 后面的 base,所以解析过程是作为一个不动点运行的,而不是单次遍历。 6. **检出 (Check out)。** 遍历 commit 的 tree,以正确的 mode 写入 blob (包括符号链接和可执行位),并写入一个 `.git/index`,它 缓存每个文件的 `stat` 数据——这正是让 `git status` 能够 即时返回的原因,也是使其报告 *干净* 的树而不是已修改的树的原因。 ## 我如何知道它是正确的 声称“它能用”很容易。测试套件 (`python -m unittest discover -s tests`) 让 Git 自己成为裁判——包含 39 个测试,没有对格式的任何 mock: - **针对 `git` 的差分测试。** 使用真正的 `git` 二进制文件构建一个 fixture 仓库,并重新打包两次——一次强制使用 `OFS_DELTA`,一次强制使用 `REF_DELTA`。nanoclone 解析生成的 packfile,并将每一个 object id、 类型和内容的每一个字节都与 `git cat-file --batch-all-objects` 进行比较。 在所有 42 个 SHA-1 哈希值上与 Git 达成一致,意味着在每一个字节上都达成一致。 - **`verify-pack` 交叉检查。** 我们基于偏移量的解析会与 `git verify-pack -v` 逐条进行比较,包括哪些条目是 delta。 - **Git 审查我们的检出。** 我们手动写入一个仓库,然后询问 Git: `git fsck --strict` 必须通过,`git status --porcelain` 必须为空,并且 `git rev-parse HEAD` 必须一致。符号链接、可执行位、空文件和 UTF-8 文件名都经过了明确检查。 - **真实的 clone。** 选择性开启 (`NANOCLONE_NETWORK_TESTS=1`) 的测试通过网络克隆一个公开的 仓库,并断言生成的 tree 哈希值与在同一 URL 上执行 `git clone` 产生的哈希值相等。 - **对抗性输入。** 损坏的校验和、截断的流、保留的 pkt-line 长度 `0003`、保留的 delta 操作码 `0x00`、运行超出其 base 末尾的复制指令,以及无法解析的 delta,全都会以 特定的错误被拒绝,而不是给出错误的答案。 ## 性能 在 `git clone` 为 `psf/requests` 获取的 13.8 MB packfile (26 818 个 object,其中 17 560 个是 delta)上测量,使用 Python 3.14,页面缓存已预热: | 阶段 | 时间 | | --- | --- | | 解压并解析每个 object | 0.28 s | | 解析所有 17 560 个 delta | 1.16 s | | 写入 26 818 个松散的 object | 7.27 s | 完整克隆该仓库(包含网络耗时)大约需要 14 秒。 早期版本在解析阶段花费了 **63 秒**。原因仅仅在于一 行代码:交给 zlib 处理的 `buf[pos:]` 会拷贝整个 packfile 的剩余部分,每处理一个 object 就拷贝一次,复杂度呈平方级增长。改为传入 `memoryview` 的固定大小块 后,该阶段的速度提升了 225 倍。这在 `packfile.py` 中做了文档说明,以便 接下来的读者不会重新引入这个问题。 ## 限制 这些都是刻意为之的——我们的目标是理解协议,而不是取代 Git: - **仅支持 HTTP(S)。** 不支持 SSH,不支持 `git://`。`scp` 风格的 URL 会被重写为 HTTPS。 - **必须使用 Protocol v2。** 每个现代主机(GitHub, GitLab, Gitea, Codeberg) 都支持它;仅支持 v0 的服务器会被拒绝并返回明确的提示信息。 - **仅限 clone。** 不支持 push,不支持增量 fetch,不进行带有 `have` 行的协商,不支持浅克隆或部分克隆。 - **仅限 SHA-1 仓库。** 不支持实验性的 SHA-256 仓库。 - **Object 以松散形式写入,** 而不是带有索引的 packfile。这样更容易 阅读,但占用了磁盘空间:`psf/requests` 的 clone 在 `.git` 中占据了 124 MB,而 真正的 Git 只需要 15 MB。这也意味着不会生成 `.idx` 文件。 - **不进行 `.gitattributes` 处理:** 不进行 CRLF 转换,没有 clean/smudge 过滤器,不支持 Git LFS。Submodule 的 commit 会记录在索引中,但不会拉取它们 的内容。 ## Auf Deutsch `nanoclone` implementiert `git clone` von Grund auf neu — ausschließlich mit der Python-Standardbibliothek, ohne jede Abhängigkeit und ohne das `git`-Binary aufzurufen. Umgesetzt sind das pkt-line-Format, der Smart-HTTP-Transport (Protokoll v2), der Packfile-Parser samt Auflösung von OFS- und REF-Deltas sowie ein Checkout, der einen gültigen Git-Index schreibt. Der Korrektheitsbeweis liegt in den Tests: Ein Repository wird mit echtem `git` gebaut und neu gepackt, nanoclone liest es, und **jede einzelne Objekt-ID, jeder Typ und jedes Byte** wird gegen `git cat-file` verglichen. Anschließend prüft `git` selbst unser Ergebnis — `git fsck --strict` muss bestehen und `git status` muss ein sauberes Arbeitsverzeichnis melden. ## 参考 实现遵循了官方的格式规范: - [pkt-line 格式](https://git-scm.com/docs/protocol-common#_pkt_line_format) - [Protocol v2](https://git-scm.com/docs/protocol-v2) - [Smart HTTP transport](https://git-scm.com/docs/http-protocol) - [Packfile 格式](https://git-scm.com/docs/gitformat-pack) - [Index 格式](https://git-scm.com/docs/index-format) ## 许可证 MIT — 见 [LICENSE](LICENSE)。
标签:Git, Python, SOC Prime, 内核驱动, 开发工具, 无后门, 网络协议, 解析器, 逆向工具