关于

Note 译者注:本文档翻译自 Git Internals,仅用于学习交流。

本 PWA 应用的内容是关于 Git 的内部结构的。

本应用在 github.com/HarshKapadia2/git_internals 上开源。

Note 该网站的内容也以讲座的形式呈现,欢迎 观看 Git Internals 讲座

前置准备

事先了解一下 Git 基础会对学习本站内容很有帮助。

.git 目录

简介

在一个目录中执行 git init 命令 时,Git 会在这个目录下创建一个隐藏目录 .git.git 目录包含了 Git 可以执行其版本控制功能的项目的所有历史数据。此外,它还包含了用于配置 Git 如何处理该特定 仓库 的相关事宜的配置文件。

.git 目录的内容

.git
├───addp-hunk-edit.diff
├───COMMIT_EDITMSG
├───config
├───description
├───FETCH_HEAD
├───HEAD
├───hooks
│   └───<*.sample>
├───index
├───info
│   └───exclude
├───lfs
│   ├───cache
│   │   └───locks
│   │       └───refs
│   │           └───heads
│   │               └───<branch_names>
│   │                   └───verifiable
│   ├───objects
│   │   └───<first_2_SHA-256_characters>
│   │       └───<next_2_SHA-256_characters>
│   │           └───<entire_64_character_SHA-256_hash>
│   └───tmp
├───logs
│   ├───HEAD
│   └───refs
│       ├───heads
│       │   └───<branch_names>
│       ├───remotes
│       │   └───<remote_aliases>
│       │       └───<branch_names>
│       └───stash
├───MERGE_HEAD
├───MERGE_MODE
├───MERGE_MSG
├───objects
│   ├───<first_2_SHA-1_characters>
│   │   └───<remaining_38_SHA-1_characters>
│   ├───info
│   │   ├───commit-graph
│   │   └───packs
│   └───pack
│       ├───multi-pack-index
│       ├───<*.idx>
│       ├───<*.pack>
│       └───<*.rev>
├───ORIG_HEAD
├───packed-refs
├───rebase-merge
│   ├───git-rebase-todo
│   ├───git-rebase-todo.backup
│   ├───head-name
│   ├───interactive
│   ├───no-reschedule-failed-exec
│   ├───onto
│   └───orig-head
└───refs
    ├───heads
    │   └───<branch_names>
    ├───remotes
    │   └───<remote_aliases>
    │       └───<branch_names>
    ├───stash
    └───tags
        └───<tag_names>

index 文件

Note
  • 该文件是在第一次添加文件时创建的,以后每次执行 git add 命令 时都会更新它。

Index file explained
  • 该文件是一个二进制文件,只是使用 cat .git/index 查看其内容时会导致乱码. 想要查看其内容可以使用 git ls-files --stage [底层命令]。

'git ls-files --stage' command
  • 上图中的内容解释

    • 100644 表示文件的模式,它是一个八进制数。

      Octal: 100644
      Binary: 001000 000 110100100
      • 前 6 个二进制位指示对象类型。

        • 001000 表示普通文件。(如本例所示。)

        • 001010 表示 符号链接.

        • 001110 表示 gitlink.

      • 接下来的 3 个二进制位 (000) 保留。

      • 最后 9 个二进制位 (110100100) 表示 Unix 文件权限

        • 644755 对于普通文件有效。

        • 对于符号链接和 gitlinks 这 9 位的值都是 0

    • 接下来的 40 个字符的十六进制字符串表示的是文件的 SHA-1 哈希

    • 下一个数字是暂存号/槽,它在处理合并冲突时很有用。

      • 0 表示正常无冲突的文件。

      • 1 表示文件的原始版本。

      • 2 表示“我们的”版本,即 HEAD 指向的版本,包括了冲突双方的更改。

      • 3 表示“他们的”版本,即被合并的版本的文件。

    • 最后的字符串是所引用文件的名称。

Note 有关 index 文件的更多内容可参阅 资源 一节。

HEAD 文件

  • 它用于引用当前分支中的最新提交。

  • HEAD 文件通常不使用这个最新 commit 的 SHA-1 哈希,而是记录了一个在 refs 目录 下的文件(以当前分支的名称命名)路径,这个文件存储了在它这个分支下最后一个 commit 的 SHA-1 哈希。

  • 一个特定的 commit 或 tag 被检出 时,该文件才会包含提交的 SHA-1 哈希。(分离头(Detached HEAD)状态。)

  • 关于 HEAD 文件的更多内容

  • Eg:

    # in the 'main' branch
    $ cat .git/HEAD
    ref: refs/heads/main
    $ git switch test_branch
    Switched to branch 'test_branch'
    $ cat .git/HEAD
    ref: refs/heads/test_branch

refs 目录

.git
├───...
└───refs
    ├───heads
    │   └───<branch_name(s)>
    ├───remotes
    │   └───<remote_alias(es)>
    │       └───<branch_name(s)>
    ├───stash
    └───tags
        └───<tag_name(s)>
  • 该目录保存了对每个本地分支中最新提交的引用,并以该提交的 SHA-1 哈希形式获取远程分支。

  • 它还存储了已经 [打了标签] 的 commit 的 SHA-1 哈希。

  • HEAD 文件 引用了该目录(refs)下的 heads 子目录的一个文件(以当前检出分支命名)。

Note 不要把 .git/refs 目录和 .git/logs/refs 目录 搞混了,他们的用处不同。

packed-refs 文件

  • Git 会在 refs 目录 下的为每个分支和标签创建一个文件。

  • 在拥有很多分支和标签的仓库中,便会存在很多引用(refs),其中很少会被主动使用或更改的。

  • 这些引用占用了大量的存储空间,以致出现性能问题。

  • git pack-refs 就是用来解决上面👆🏻这个问题的。它将所有的引用存储在这个名为“packed-refs”的文件中。git gc 命令也可以用来执行此操作。

Print the packed-ref file
  • 执行上述命令打包后,如果某个引用在 refs 目录中缺失了,Git 就会在此文件中查找。

  • 对已打包 ref 的分支进行后续更新(新提交、拉取或推送更改)时,会像往常一样在 refs 目录中创建一个带有该分支名称的新文件,但它不会将该分支在 packed-refs 文件中的哈希值更新为最新的哈希值。(为此必须生成一个新的 packed-refs 文件。)

logs 文件夹

.git
├───...
└───logs
    ├───HEAD
    └───refs
        ├───heads
        │   └───<branch_name(s)>
        ├───remotes
        │   └───<remote_alias(es)>
        │       └───<branch_name(s)>
        └───stash
  • 该目录包含了按顺序排列的所有提交的历史记录。

Print a branch’s log file
  • 每一行内容按照从左到右的顺序分别是:父提交的 SHA-1 哈希,当前提交的 SHA-1 哈希,提交者的名字和邮箱,提交的 Unix 时间戳,时区,行为类型和消息。

  • 本地 Git 仓库中的每个分支以及从单个或多个远程 Git 仓库(如果有的话)获取的分支都有日志。

  • 进入 logs 目录

    • HEAD 文件存储了用户执行的所有命令的信息,像分支切换、提交、变基等。

    • refs 目录中仅包含特定于分支的操作和历史,例如提交、拉取、重置、变基等。

Note 不要把 .git/logs/refs 目录 and the .git/refs 目录 搞混了,他们的用处不同。

FETCH_HEAD 文件

  • 它包含了取回的远程分支的最新提交。

  • 它对应的分支是:

    • 上次取回时 检出 的分支。

      The contents of the FETCH_HEAD file

      • 从上图可以看到,只有一个分支不带 not-for-merge 文本描述。这个奇怪的分支(本例中是 main 分支)就是远程取回时检出的分支。

    • 使用 git fetch <remote_repo_alias> <branch_name> command 命令时明确指定的分支。

      The contents of the FETCH_HEAD file

COMMIT_EDITMSG 文件

  • 提交信息就是写入此文件中的。

  • 当执行 git commit command 命令时,该文件会被编辑器打开。

  • 该文件内容包含 git status command 命令输出中由该字符 # 注释掉的内容。

  • 如果之前有过提交,那么这个文件将会显示上次提交的信息以及本次提交之前的 git status 的输出。

objects 目录

.git
├───...
└───objects
    ├───<first_2_SHA-1_characters>
    │   └───<remaining_38_SHA-1_characters>
    ├───info
    │   ├───commit-graph
    │   └───packs
    └───pack
        ├───multi-pack-index
        ├───<*.idx>
        └───<*.pack>
  • .git 目录下最重要的目录了。

  • 它保存着仓库中所有的 commit, tree and blob 对象 的数据(SHA-1 哈希)。

  • 为了减少访问时间,对象被放置在存储桶(目录)中,其 SHA-1 哈希值的前两个字符作为存储桶的名称,其余 38 个字符用于命名对象的文件。

  • 关于 pack 目录的更多内容

Note 不要将这个目录 (.git/objects/info) 和 .git/info 目录 搞混了,它们的用处不同。

info 目录

.git
├───...
└───info
    └───exclude
Note 不要将这个目录 (.git/info) 和 .git/objects/info 目录 搞混了,它们的用处不同。

config 文件

addp-hunk-edit.diff 文件

  • 当在 git add --patch 命令 中指定了 -e (edit) 选项时创建该文件。

  • 该文件的创建使得你可以手动编辑将要 暂存 的文件 hunk。

ORIG_HEAD 文件

  • 它包含了一个 commit 的 SHA-1 哈希。

  • 它是 HEAD 的前一个状态,但不一定是紧邻的前一个状态。

  • 它是由某些具有破坏性/危险行为的命令设置的,因此它通常指向具有破坏性更改的最新提交。

  • 由于 [git reflog 命令] 的存在,它现在不是那么有用,git reflog 更容易回退/重置到一个特定的 commit。

description 文件

  • 仓库的描述文件。

  • 该文件由 GitWeb 使用,目前几乎没有人使用它,因此可以不用管。

Git 对象

简介

Git 有两种数据结构, 一种是可变的(mutable) index,用于缓存有关工作区和下次要提交的修订的信息。另一种是不变的(immutable)、只能追加的对象数据库 (repository),它包含四类对象:

  • Blob 对象

  • Commit 对象

  • Tree 对象

  • Tag 对象

Git 使用这些对象来存储数据来以进行版本控制,通过理解这些对象可以理解 Git 的内部工作原理。

本节将通过一个示例来探讨 Git 内部工作的部分内容。

Note 接下来,请跟随我们的脚步随时使用 Git Graph 创建图解。

运行 git init`inside_git 目录(根目录)中初始化一个空 仓库,Git 在根目录中会创建一个隐藏目录 .git

git init

命令 du -c 用于列出 inside_git 的子目录及其占用的磁盘大小(以 kbs 为单位)。

du -c

blob 对象

Note Blob 对象存储了文件的内容。

在根目录下创建一个文件。

Create new file

现在,工作树(根目录)包含了 .git 目录和新文件 master_file_1.txt

Master File

使用 git add . 命令将新文件添加到 暂存区,然后再次运行 du -c

Stage file

注意一个新目录 e6 被添加到了 .git/objects 目录下。

使用 dir (或 ls) 命令来看下 .git/objects/e6 目录下的文件。

Create new directory

文件名 9de29bb2d1d6434b8b29ae775ad8c2e48c5391 38 个字符长。将其附加到文件夹名称 (e6) 后,就变成了 40 个字符的字符串 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391,这是一个 SHA-1 哈希。Git 使用 SHA-1 算法对文件内容(以及其他相关数据)进行哈希处理,生成 40 个字符的十六进制字符串。对于每一个 暂存,[commit] 和 [tag] 都会生成自己唯一的 SHA-1 哈希值。(作为 40 个字符的字符串,哈希冲突非常罕见。)哈希值的前两个字符用于将哈希值归类到文件夹中,以减少访问时间。为了方便起见,Git 有时只使用对象哈希值的 4 到 8 个字符。

如前所述,Git 会对文件内容以及其他细节进行散列处理,生成一个 40 个字符的 SHA-1 哈希值。为了验证这一点,我们需要在文件中添加一些内容,然后再次把文件添加到暂存区。(这将产生另一个哈希)。

Add to Master file
Edit master file

从上图中的最后一条命令可以推断,一个新的哈希值 1a3851c172420a2198cf8ca6f2b776589d955cc5 生成了。使用 cat 命令检查其内容:

Check contents

可以看到其输出是乱码,因为 Git 使用 zlib 库压缩文件内容(以及一些附加数据),然后将其存储在文件中。因此,为了弄懂这些乱码是什么意思,我们需要解压缩文件的内容。

Decompress

可以看到,这个哈希文件的内容是 blob 16\0Git is amazing!\n。(\0\n 不可见,下文会对其解释。)

将这段内容拆开来看:

  • blob 是文件的对象类型,它是 'Binary Large OBject' 的缩写。这种对象 (文件) 存储了文件的内容。

  • 16 是文件的尺寸(长度)。Git is amazing! 由 15 个字符组成, 然而 echo 命令 在文本的末尾添加了一个换行符 (\n),因此其长度是 16。

  • 就像在输出中看不到 \n 字符一样,在长度和文件内容之间有一个 NULL 字符 (\0)。

  • Git is amazing!\n 是文件内容 (\n 不可见)。

Note

如果 blob 16\0Git is amazing!\n 是使用 SHA-1 哈希的, 那么下面也会生成相同的哈希值 (1a3851c172420a2198cf8ca6f2b776589d955cc5):

Generating hash for the string

综上,Git 使用 <object_type> <content_length>\0<file_content> 这个字符串格式来生成文件的哈希值,然后将其压缩后存在文件中。 (文件名是生成的 40 个字符哈希值的最后 38 个字符,前两个字符则用于存储它的文件夹名。)

Note Blob 对象不存储文件的差异化内容( diff/delta),而是存储文件的完整内容。
Tip

使用 cat 命令查看文件内容的过程非常繁琐,我们最好使用 Git 提供的 git cat-file [底层命令] 命令。

将使用的 git cat-file 命令的变体

接下来要使用 git cat-file 的几种形式:

  • git cat-file -p <hash> (-p = pretty print) 用于显示文件内容。

  • git cat-file -t <hash> (-t = type) 用于显示文件对象类型 (blob, commit, tree 或 tag)。

  • git cat-file -s <hash> (-s = size) 用于显示文件尺寸。

commit 对象

Note Commit 对象将若干个 tree 对象链接成一个历史记录,它包含了一个 tree 对象(顶级源目录)的名称,一个时间戳,一条日志信息以及 0 个或多个父 commit 对象的名称。

提交 master_file_1.txt,然后再次运行 du -c

Commit master file

从上图可以看到,有两个新目录 .git/objects/1b.git/objects/d5 被创建了。而且,文件被提交后,Git 输出了这个提交的哈希值的前 7 个字符。

以这 7 个字符为参,用 git cat-file -t 命令检查文件类型:

Plumbing commands

文件类型正是 commit,从而可知该文件是通过一个提交生成的。

使用 git cat-file -p 命令输出这个 commit 对象的内容如下:

Commit

Commit 对象的内容解析:

  • tree 1b2190cdc2801ec3df6505dc351dee878ac7f2fc 是生成的另一个 SHA-1 哈希值(还记得提交文件时在 .git/objects 生成的两个目录吧),其对象类型是 tree,它就是当前仓库状态的 [快照(snapshot)]。

  • 父提交的 SHA-1 哈希(此处不存在,下面会解释。)

  • 下一行是作者的详细信息 (写代码的人):

    • 姓名

    • 邮箱 ID

    • 时间戳

  • 下一行是提交者的详细信息 (提交代码的人):

    • 姓名

    • 邮箱 ID

    • 时间戳

  • 提交信息

  • 提交描述 (如果提供的话,这里没有。)

tree 对象

Note 一个树对象就相当于一个(子)目录:它包含一个文件名列表,每个文件名都有一些类型位和一个 blob 或 tree 对象的名称,即这个文件、符号链接或目录的内容。这种对象描述了源目录树的一个快照。

让我们看一下在 commit 对象(文件)中列出的 tree 文件的内容:

Check contents

Tree 对象类型的文件包含了本地仓库在一个快照(当前状态)中的文件和目录条目。每行的格式是相同的。

Tree 对象的内容格式如下:

  • 100644 表示文件的模式,是一个八进制数。

    Octal: 100644
    Binary: 001000 000 110100100
    • 前 6 个二进制位表示文件类型。

      • 001000 代表普通文件 (如本例所示)。

      • 001010 代表 符号链接.

      • 001110 代表 a gitlink.

    • 接下来的 3 个二进制位 (000) 暂未使用。

    • 最后 9 个二进制位 (110100100) 表示 Unix 文件权限

      • 644755 对于普通文件有效。

      • 对于符号链接和 gitlinks 这 9 位的值都是 0

  • blob 标识对象类型 (也可以是 tree 对象 下文会解释。)

  • 1a3851c172420a2198cf8ca6f2b776589d955cc5 是文件 SHA-1 哈希值。

  • 文件名称。

综上,每个 commit 对象都指向一个 tree 对象,每个 tree 对象都指向若干 blob 以及/或 tree 对象,分别对应具体的文件和子目录。

目前,commit、tree 和 blob 文件的连接关系如下所示:(HEAD 只是一个指向最新提交的指针。)

Connection graph
Note
  • Blob e69de 已经被更新为 1a385 了,因此它没有被连接到 tree 1b219。对于每个已添加文件来说,只有其最新的 blob 才连接到一个伴随 commit 产生的新 tree 对象上。

  • 上图可以使用 Git Graph 创建。

父提交

再新建一个文件 (master_file_2.txt),暂存并提交。

Create master file

看下 commit 对象类型文件的内容(使用上图中的部分哈希 8282663):

Create another master file

可以看到一个新行 parent d5b8f77ce1dc1a37b29885026055c8656c3e0b65 出现了。记着,这是前一个提交的哈希。Git 因此又创建了一个关系图,准确地说,应该是有向无环图。(图在下面)

而且,HEAD 会自动指向这个最新的提交 82826,而不是父提交(前一个-d5b8f)。看下 HEAD 的指向来验证下:

HEAD

它确实指向最新提交 (82826)。

现在再看下最新提交指向的 tree 对象文件的内容:

Contents of tree

Commit 对象,tree 对象以及 HEAD 的最新关系连接图如下所示:

Connection graph
Note 该图可以使用 Git Graph 创建。

新建目录

在 (dir_1) 目录下新建一个文件 (master_dir_1_file_3.txt),暂存并提交,然后看下 commit 对象类型文件的内容:

Create new file in directory

该文件内功的格式与 上文介绍的相同

Tree 文件(上图中的哈希 f6a65)的内容如下:

Contents of tree

令人惊讶的是,树 f6a65 指向另一棵树 abecf!新树的名称是 dir_1

再看下 dir_1 树的内容:

Contents of directory tree

它指向 dir_1 目录下的文件 (master_dir_1_file_3.txt)。

来看下树 f6a65 是如何与其他树和 blob 建立连接的:

Tree

目前仓库的关系图是:

Connection Graph
Note 该图可以使用 Git Graph 创建。

重命名文件

master_file_1.txt 重命名为 the_master_file.txt 来看下 Git 内部是如何处理它的。

Rename file
Stage

提交文件时,Git 会识别出文件已重命名,而不是新建一个文件,如上图最后一行所示。之所以能识别到文件重命名,是因为文件的 SHA-1 哈希值没有变化(因为文件内容没有变化)。

Check the contents of the commit and tree files.

Contents of commit

从最后一行可以看出,哈希 1a385 与原始文件相同(文件名是 master_file_1.txt)。Git 只是在 commit 指向的树文件中记录了已经变更后的文件名称,而不是新建一个 blob 文件。这就是 Git 高效的空间管理!

当前仓库的结构如下所示:

Connection Graph
Note 该图可以使用 Git Graph 创建。

修改大文件

添加图像并将其提交到 Git。图像的大小为 1.374Mb(或 1374kb),因此与其他文件(约 1 kb/文件)相比,它是一个相对较大的文件。

Stage
Commit

稍微修改下图片文件的内容,然后再次暂存并提交。

Stage and commit

master_image_1.png 在最新树 6d2d2 和先前的树 27666 中的 SHA-1 哈希并不相同,因此 Git 为这个示例文件先后创建了两个不同的 blob 对象(ca8931f7af),即便它们之间只存在细微的差别。

现在运行 du -c

Du -c

从上图可以看到,这两个目录 (.git/objects/1f.git/objects/ca) 的大小一样 (1376 kb)。

Note 目录内容的大小 (1376 kb) 大于图像大小 (1374 kb),因为 Git 将文件类型和大小(长度)添加到 blob 文件后才对其进行哈希处理。

那么 Git 处理大型文件的效率就低吗?当然不是。文件的内容发生了变化,产生的 SHA-1 哈希值(1f7af)与原来的 SHA-1 哈希值(ca893)不同,因此 Git 无法像处理重命名文件那样简单来处理这种变化。在本地版本库中拥有如此巨大的文件的多个副本并不是问题,但从 GitHub 等平台 推送拉取 则会占用大量带宽。为了避免这种情况,Git 使用了 Delta Compression 技术,它会存储旧文件与新文件的差异(diff),并将新文件作为父文件。这一点将在下面的小节中讨论。

pack 目录

.git
├───...
└───objects
    ├───...
    └───pack
        ├───multi-pack-index
        ├───<*.idx>
        └───<*.pack>

每次执行 clonepushpull,或者运行垃圾收集 (git gc) 时,都会执行增量压缩(Delta compression)

增量压缩会在 .git/objects/pack 目录下创建两种类型的文件。

  • 若干 Pack (.pack) 文件

  • 若干 Index (.idx) 文件

Note
  • 一个仓库可以有多个 Packfiles。

  • 每个 Packfile 对应一个 Index 文件。

  • 在极端情况下也可能会创建 multi-pack-index (MIDX) 文件,但这里不考虑。

仓库的当前状态:

Du -c

注意上图中 .git/objects/pack 目录的大小是 0kb。

垃圾回收(git gc)可以用来执行增量压缩,之后用 du -c 来观察变化。

Du -c

请注意,上图中 .git/objects/pack 的大小从 0kb 变成了 1380kb,.git/objects 中的很多文件也消失了,只留下了 .git/objects/e6

Note .git 目录的总大小从 4220kb(见本小节的第一个 du -c 图像)减少到 2838kb(如上图所示),这使得本地仓库的大小减少了 32.75%

The contents of .git/objects/pack

Content of directory

As mentioned above, two types of files (a pack .pack file and an index .idx file) are created in .git/objects/pack.

Check the contents of the Packfile using the plumbing command git verify-pack -v path/to/pack/file/<file_name>.pack (-v = verbose).

Contents of files

From the above image, it can be understood that the Packfile contains all the Git objects. The Pack file is a file that contains all the Git Objects (along with their content) stored in it in a particular format. All the objects stored in the Packfile are removed from the .git/objects directory.

From the above image, it can also be understood that the size of the newly modified image (hash 1f7af) is very large in comparison to the original image (hash ca893). The blob of the original image (hash ca893) also has the hash of the modified image (1f7af) mentioned after it, indicating that its parent is the newly modified image file (hash 1f7af). Thus Git stored the entire new file and only a diff/delta for the older file with a pointer to the newer file, rather than storing the entire file again, making it space efficient.

Note The newer file (hash 1f7af) will usually be accessed more than the older one (hash ca893), so storing the entirety of the newer file and a delta/diff for the older one makes more sense than storing the entirety of the old file and a delta/diff for the new one. As the newer file will usually be accessed more, it would be inefficient to apply the delta/diff of the newer file to the entirety of the older file to generate the newer file every time. It is cheaper to apply the delta/diff of the older file to the entirety of the newer file, as the older file won’t be accessed as frequently.
Note

On running aggressive Garbage Collection (git gc --aggressive), Git got rid of all the files in .git/objects that were referenced in a tree and added them to the Pack file. The .git/objects/e6 directory did not get removed as it was not referenced (listed) in any Tree Object.

As mentioned at the start of this sub-section, these Packfiles and Index files are created every time a clone, push or pull is executed, or if Garbage Collection (git gc) is run. Why is this so? Network bandwidth and clone/push/pull command execution time are the main reasons. Applying Delta compression and putting in all objects into one file makes it simpler and faster to transfer data over the Network and also saves storage space (~32% space was saved through packing in this case).

Take a look at the log of the repository.

Log of repository
Note Further reading on Packfiles can be found in the Resources section.

空提交

git commit^ 的 --allow-empty 选项 允许创建不含有任何文件变更的提交记录。

由于空提交没有对任何文件进行更改,因此它们始终指向 分支最新树对象

为了说明这一点,请使用以下命令设置仓库:

$ git init
$ touch file_1.txt
$ git add .
$ git commit -m "Add file_1.txt"
$ git commit --allow-empty -m "Empty commit #1"
$ git commit --allow-empty -m "Empty commit #2"

# Now run
$ git log --oneline --graph
* 208cead (HEAD -> main) Empty commit #2
* 64cf914 Empty commit #1
* be0c1ec Add file_1.txt

Use the git cat-file -p <hash> command as done in previous sub-sections to create the graph.

上面仓库的图解如下:

Connection Graph
Note
  • 如果空提交是存储库中的第一个提交(初始提交),那么它将有自己的空树对象与之关联。在所有其他情况下,空提交将指向分支中最新的树对象。

  • 该图可以使用 Git Graph 创建。

资源