从零开始写 Git(0)

什么是 Git?

在 Linus 看来,Git 可以代表很多东西——心情好时是 “global information tracker”,崩溃时则是 “goddamn idiotic truckload of sh*t”。它是 Linus Torvalds 在 2005 年 4 月用大概 7 天写出来的分布式版本控制工具。创造出来的目的是让 Linux 内核的开发变得便捷(而不是用邮件来发送源码)。随着近些年的发展,Git 已经是全球开发者和软件工程师不得不日常使用的一个工具。建立在 Git 之上的 GitHub 已经是全球最大的代码托管平台。

到底什么是 Git?

我先来讲一个我认识的人的事。ta 每次写作文的时候都喜欢写很多不同的版本(初稿、第二稿、终稿、终终稿、绝对不改稿、绝对绝对不改-终稿……)。每次写的时候都必须复制一个上一版本的作文来进行修改。

如果是编程的话,情况就会更加复杂。修改一个 bug 可能会导致更多 bug 的出现。可能我修到一半不想修了想回去,我都不知道哪个才是我一开始的版本。每次都手动复制粘贴的话,人早就迷失了。

而 Git 就是把复制存储这些自动化,并且加了些小功能,比如 diff(可以看到和上个版本的区别)、log(可以看到不同版本之间的依赖关系)等等。

为什么我要重新写 Git?

早些时候,我在油管上刷视频的时候,看到了 ThePrimeagen 在说 Linus Torvalds 在没有 AI、没有 LLM 的情况下用 7 天就写出了一个能跑的 Git 版本。而现在的计科学生有 ChatGPT、有 Claude 的帮助,各种文章来解释 Git 的原理,重新写一个 Git 的克隆版十分简单,最多一两个月就可以做到。这个过程也能让人了解 Git 的实际工作原理和基本的文件系统操作(如创建、删除文件/文件夹,计算 SHA-1,文件路径搜索,读写文件等)。

当时的我又对 Rust 十分感兴趣,于是就打算用 Rust 来写一个 Git 实现。在开写之前,我找 Claude 验证了一下这个项目的可行性:

Claude discussing writing Git in Rust

理论可行,并且写 Git 并不需要使用复杂的 lifetime、borrow checker、async 等 Rust 生态内比较困难的特性,所以并没有一个十分复杂的学习曲线。

Git 的实现方式

在 Git 里,所有东西都是一个 object 存储在 .git/objects 文件夹里面。

以下是存储在 objects 文件夹里面的 4 种 object:

  1. Blob:一个 blob 代表一个文件,用 bytes string 存储 blob <size>\0<content>,再用 SHA-1 来生成独属于每个文件状态的哈希值,相当于一个对文件状态的快照。通过 SHA-1 可以在 objects 目录中找到对应的文件内容。原始的文件名则存储在引用该 blob 的 tree object 中。

  2. Commit:这是一般使用 Git 的人接触最多的 object(git commit)。一个 commit 由这几个部分组成:

    • 代表目前 repository 状态的 tree
    • 代表上一个 commit 的 parent(如果是第一个 commit 的话,此行为空)
    • 作者
    • committer(早些时候的 Git 并没有 remote,开发者都是用邮件来传达源码的,所以会导致编写源码的人和提交的人并不是同一个人)
    • commit 名称
  3. Tag:一个 tag 是一个指向某个 commit 的引用,通常用于标记发布版本(如 v1.0、v2.0)。它也可以附带一个标签信息(annotated tag),包含作者、日期和签名等内容。

  4. Tree:一个 tree 里带有很多个 blob,相当于对当前 Git worktree 的一个整体快照。Tree 也拥有独属于自己的 SHA-1 编号。

A flowchart showing .git/objects internal implementation

Plumbing vs. Porcelain

在 Git 的日常使用命令当中:commitpushaddresetswitchpull 都属于 porcelain 命令。它们并不会直接操作底层的 object。这些也是 Linus 在写完 Git 转交给 Junio Hamano 之后才增加的便于日常使用的命令。

Linus 在那七天主要是写出了 Git 的 plumbing 命令:cat-filehash-objectls-treeloginit 等直接操作单个 object 的命令。而这些命令也是此文要实现的目标。

为什么要用 Rust 来写?

Linus 的 Git 是用 C 来实现的。C 的特点是底层并且要求开发者手动控制内存。这就导致 C 在运行时十分快速,但在开发者疏忽的情况下容易发生内存泄漏等 bug。

其他语言(如 Go、JavaScript、Python)则使用垃圾收集器来在程序运行时自动回收内存,但缺点是垃圾收集器会导致程序运行速度的减慢。

而 Rust 通过特色的 borrow checker 和 lifetime 机制,达到了在编译时就可以防止内存泄漏的效果。这就让 Rust 有 C 一样快的速度和 Go 一样的安全特性。

其实就是入了 Rust 邪教

总之,这个 Git project 就是一个让我上手 Rust 的小练手项目。那让我们开始吧!

Rust 开发环境配置

通用配置(macOS / Linux)

如果你使用 macOS 或常见的 Linux 发行版(Debian、RedHat、Arch 等),可以直接用一行命令安装:

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

你也可以使用系统的软件包管理器来安装(如 Homebrew、apt、dnf、pacman 等)。具体如何安装可以参考 Rust 官网 和你的发行版对应的 wiki。

我的配置(NixOS)

如果你不用 NixOS,下面的内容不太通用,可以跳过。

我个人使用 NixOS 来作为我的日常使用和开发的发行版。它的特性是可复刻性和声明式配置方法。通过类似于 Git 的 SHA-1 的方式在 /nix/store 里来管理不同软件包的版本和依赖。总的来说,就是一般系统的安装 Rust 方法不适用于我的系统。

我安装 Rust 开发环境的方法是用 Nix flake 来创造出一个 dev shell,再通过 direnv 让我进入这个项目的时候就自动进入 dev shell。如果你写过 JavaScript / Go / Rust / Python 等语言的话,你可以把 flake.nix 看作是 package.json / go.mod / Cargo.toml / pyproject.tomlflake.nix 也会生成一个 flake.lock,就像是 package-lock.json / go.sum / Cargo.lock / poetry.lock 这些锁定依赖软件包的工具。

当然我不需要从零开始写一个开发 Rust 的 Nix flake。Nix 自己自带了一个开发 Rust 的模板。在文件夹里面打开 shell 运行:

nix flake init -t templates#rust

就会生成以下的 flake.nix

{
  inputs = {
    naersk.url = "github:nix-community/naersk/master";
    nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
    utils.url = "github:numtide/flake-utils";
  };

  outputs =
    {
      self,
      nixpkgs,
      utils,
      naersk,
    }:
    utils.lib.eachDefaultSystem (
      system:
      let
        pkgs = import nixpkgs { inherit system; };
        naersk-lib = pkgs.callPackage naersk { };
      in
      {
        defaultPackage = naersk-lib.buildPackage ./.;
        devShell =
          with pkgs;
          mkShell {
            buildInputs = [
              cargo
              rustc
              rustfmt
              pre-commit
              rustPackages.clippy
            ];
            RUST_SRC_PATH = rustPlatform.rustLibSrc;
          };
      }
    );
}

之后只要在这个 flake.nix 所在的目录运行:

nix develop

就会生成一个 flake.lock 和一个安装了 cargo(Rust 包管理器)、rustc(Rust 编译器)、rustfmt(Rust formatter)、clippy(Rust linter)的开发环境。

如果你嫌每次进入开发环境都需要输入命令太麻烦的话,你可以使用 direnv 来自动化这个命令。创建一个 .envrc 文件在根目录里,并且输入:

use flake

就可以自动进入开发环境了。

小结

这篇文章简单概括了:

  • Git 的作用
  • 我重写 Git 的理由
  • Git 的内部对象模型(Blob、Commit、Tag、Tree)
  • Plumbing 与 Porcelain 命令的区别
  • Rust 的特性
  • 开发环境的配置

下篇文章将正式开始重写 Git。

使用 Hugo 构建
主题 StackJimmy 设计