9. Git - 内部原理 - Notes

9 Git 内部原理

  1. 底层命令 (Plumbing) 和高层命令 (Porcelain)
  2. 9.2 Git 对象
  3. 9.3 Git References
  4. 9.4 Packfiles
  5. 9.5 The Refspec
  6. 9.6 传输协议
  7. 9.7 维护及数据恢复
  8. 9.8 总结

9.1 底层命令 (Plumbing) 和高层命令 (Porcelain)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sebUrxFu-1586850656647)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20200414101854974.png)]

config 文件包含了项目特有的配置选项,info 目录保存了一份不希望在 .gitignore 文件中管理的忽略模式 (ignored patterns) 的全局可执行文件。hooks 目录保存了第七章详细介绍了的客户端或服务端钩子脚本。

另外还有四个重要的文件或目录:HEADindex 文件,objectsrefs 目录。这些是 Git 的核心部分。objects 目录存储所有数据内容,refs 目录存储指向数据 (分支) 的提交对象的指针,HEAD 文件指向当前分支,index 文件保存了暂存区域信息。马上你将详细了解 Git 是如何操纵这些内容的。

9.2 Git 对象

Git 是一套内容寻址文件系统。很不错。不过这是什么意思呢? 这种说法的意思是,Git 从核心上来看不过是简单地存储键值对(key-value)。它允许插入任意类型的内容,并会返回一个键值,通过该键值可以在任何时候再取出该内容。可以通过底层命令 hash-object 来示范这点,传一些数据给该命令,它会将数据保存在 .git 目录并返回表示这些数据的键值。首先初使化一个 Git 仓库并确认 objects 目录是空的:

Git 初始化了 objects 目录,同时在该目录下创建了 packinfo 子目录,但是该目录下没有其他常规文件。我们往这个 Git 数据库里存储一些文本:

$ echo 'test content' | git hash-object -w --stdin
    d670460b4b4aece5915caf5c68d12f560a9fe3e4

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5HxyG8bn-1586850656649)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20200414110840101.png)]

参数 -w 指示 hash-object 命令存储 (数据) 对象,若不指定这个参数该命令仅仅返回键值。--stdin 指定从标准输入设备 (stdin) 来读取内容,若不指定这个参数则需指定一个要存储的文件的路径。该命令输出长度为 40 个字符的校验和。这是个 SHA-1 哈希值──其值为要存储的数据加上你马上会了解到的一种头信息的校验和。现在可以查看到 Git 已经存储了数据:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5MB1ZpaI-1586850656650)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20200414111010854.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bh37jlcA-1586850656651)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20200414111026790.png)]

可以在 objects 目录下看到一个文件。这便是 Git 存储数据内容的方式──为每份内容生成一个文件,取得该内容与头信息的 SHA-1 校验和,创建以该校验和前两个字符为名称的子目录,并以 (校验和) 剩下 38 个字符为文件命名 (保存至子目录下)。

通过 cat-file 命令可以将数据内容取回。该命令是查看 Git 对象的瑞士军刀。传入 -p 参数可以让该命令输出数据内容的类型:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4fephll9-1586850656652)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20200414111239539.png)]

可以往 Git 中添加更多内容并取回了。也可以直接添加文件。比方说可以对一个文件进行简单的版本控制。首先,创建一个新文件,并把文件内容存储到数据库中:

$ echo 'version 1' > test.txt
    $ git hash-object -w test.txt

接着往该文件中写入一些新内容并再次保存:

$ echo 'version 2' > test.txt
    $ git hash-object -w test.txt

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lVEmaiv9-1586850656652)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20200414111449974.png)]

数据库中已经将文件的两个新版本连同一开始的内容保存下来了:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gTnK0GXZ-1586850656653)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20200414112211505.png)]

再将文件恢复到第一个版本:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qpjjXsjo-1586850656654)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20200414112325190.png)]

或恢复到第二个版本

需要记住的是几个版本的文件 SHA-1 值可能与实际的值不同,其次,存储的并不是文件名而仅仅是文件内容。这种对象类型称为 blob 。通过传递 SHA-1 值给 cat-file -t 命令可以让 Git 返回任何对象的类型:

$ git cat-file -t 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a
    blob

tree (树) 对象

接下去来看 tree 对象,tree 对象可以存储文件名,同时也允许存储一组文件。Git 以一种类似 UNIX 文件系统但更简单的方式来存储内容。所有内容以 tree 或 blob 对象存储,其中 tree 对象对应于 UNIX 中的目录,blob 对象则大致对应于 inodes 或文件内容。一个单独的 tree 对象包含一条或多条 tree 记录,每一条记录含有一个指向 blob 或子 tree 对象的 SHA-1 指针,并附有该对象的权限模式 (mode)、类型和文件名信息。以 simplegit 项目为例,最新的 tree 可能是这个样子:

从概念上来讲,Git 保存的数据如图 9-1 所示。

img

你可以自己创建 tree 。通常 Git 根据你的暂存区域或 index 来创建并写入一个 tree 。因此要创建一个 tree 对象的话首先要通过将一些文件暂存从而创建一个 index 。可以使用 plumbing 命令 update-index 为一个单独文件 ── test.txt 文件的第一个版本 ── 创建一个 index 。通过该命令人为的将 test.txt 文件的首个版本加入到了一个新的暂存区域中。由于该文件原先并不在暂存区域中 (甚至就连暂存区域也还没被创建出来呢) ,必须传入 --add 参数;由于要添加的文件并不在当前目录下而是在数据库中,必须传入 --cacheinfo 参数。同时指定了文件模式,SHA-1 值和文件名:

在本例中,指定了文件模式为 100644,表明这是一个普通文件。其他可用的模式有:100755 表示可执行文件,120000 表示符号链接。文件模式是从常规的 UNIX 文件模式中参考来的,但是没有那么灵活 ── 上述三种模式仅对 Git 中的文件 (blobs) 有效 (虽然也有其他模式用于目录和子模块)。

现在可以用 write-tree 命令将暂存区域的内容写到一个 tree 对象了。无需 -w 参数 ── 如果目标 tree 不存在,调用 write-tree 会自动根据 index 状态创建一个 tree 对象。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nTFIOXVb-1586850656655)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20200414113352245.png)]

再根据 test.txt 的第二个版本以及一个新文件创建一个新 tree 对象:

$ echo 'new file' > new.txt
    $ git update-index test.txt
    $ git update-index --add new.txt

这时暂存区域中包含了 test.txt 的新版本及一个新文件 new.txt 。创建 (写) 该 tree 对象 (将暂存区域或 index 状态写入到一个 tree 对象),然后瞧瞧它的样子:

$ git write-tree
    0155eb4229851634a0f03eb265b69f5a2d56f341
    $ git cat-file -p 0155eb4229851634a0f03eb265b69f5a2d56f341
    100644 blob fa49b077972391ad58037050f2a75f74e3671e92 new.txt
    100644 blob 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a test.txt

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-odRbAWjj-1586850656656)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20200414113739018.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-w8DEqsd2-1586850656657)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20200414113755579.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bpzU7LVT-1586850656657)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20200414113810433.png)]

请注意该 tree 对象包含了两个文件记录,且 test.txt 的 SHA 值是早先值的 “第二版” (1f7a7a)。来点更有趣的,你将把第一个 tree 对象作为一个子目录加进该 tree 中。可以用 read-tree 命令将 tree 对象读到暂存区域中去。在这时,通过传一个 --prefix 参数给 read-tree,将一个已有的 tree 对象作为一个子 tree 读到暂存区域中:

$ git read-tree --prefix=bak d8329fc1cc938780ffdd9f94e0d364e0ea74f579
    $ git write-tree
    3c4e9cd789d88d8d89c1073707c3585e41b0e614
    $ git cat-file -p 3c4e9cd789d88d8d89c1073707c3585e41b0e614
    040000 tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579 bak
    100644 blob fa49b077972391ad58037050f2a75f74e3671e92 new.txt
    100644 blob 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a test.txt

如果从刚写入的新 tree 对象创建一个工作目录,将得到位于工作目录顶级的两个文件和一个名为 bak 的子目录,该子目录包含了 test.txt 文件的第一个版本。可以将 Git 用来包含这些内容的数据想象成如图 9-2 所示的样子。

img

commit (提交) 对象

你现在有三个 tree 对象,它们指向了你要跟踪的项目的不同快照,可是先前的问题依然存在:必须记往三个 SHA-1 值以获得这些快照。你也没有关于谁、何时以及为何保存了这些快照的信息。commit 对象为你保存了这些基本信息。

要创建一个 commit 对象,使用 commit-tree 命令,指定一个 tree 的 SHA-1,如果有任何前继提交对象,也可以指定。从你写的第一个 tree 开始:

C:\Users\Administrator\Documents\GitHub\playground\test>echo 'first commit' | git commit-tree 2ad71179c01caad8793a4d0eabc2d851b9050269
965eb252160c7f8579af6a7b263031bce3a9e5d4

通过 cat-file 查看这个新 commit 对象:

C:\Users\Administrator\Documents\GitHub\playground\test>git cat-file -p 965e
tree 2ad71179c01caad8793a4d0eabc2d851b9050269
author Harrish He <harrish@wicresoft.com> 1586841657 +0800
committer Harrish He <harrish@wicresoft.com> 1586841657 +0800

'first commit'

commit 对象有格式很简单:指明了该时间点项目快照的顶层树对象、作者/提交者信息(从 Git 设置的 user.nameuser.email中获得)以及当前时间戳、一个空行,以及提交注释信息。

接着再写入另外两个 commit 对象,每一个都指定其之前的那个 commit 对象:

每一个 commit 对象都指向了你创建的树对象快照。出乎意料的是,现在已经有了真实的 Git 历史了,所以如果运行 git log 命令并指定最后那个 commit 对象的 SHA-1 便可以查看历史:

C:\Users\Administrator\Documents\GitHub\playground\test>git log --stat 678b3
commit 678b391244ff71197797c7edd9aa5c0de736eee5
Author: Harrish He <harrish@wicresoft.com>
Date:   Tue Apr 14 13:59:43 2020 +0800

    'third commit'

 bak/test.txt | 1 +
 1 file changed, 1 insertion(+)

commit 934336b905b6f408ffbc742f77b1aa262aa3585e
Author: Harrish He <harrish@wicresoft.com>
Date:   Tue Apr 14 13:57:59 2020 +0800

    'second commit'

 new.txt  | 1 +
 test.txt | 2 +-
 2 files changed, 2 insertions(+), 1 deletion(-)

commit 7e5b4203c5995d277777d26ef62f7f5aa21c4586
Author: Harrish He <harrish@wicresoft.com>
Date:   Tue Apr 14 13:54:48 2020 +0800

    'first commit'

 test.txt | 1 +
 1 file changed, 1 insertion(+)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YyiSxQiU-1586850656659)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20200414140330132.png)]

这基本上就是运行 git addgit commit 命令时 Git 进行的工作 ──保存修改了的文件的 blob,更新索引,创建 tree 对象,最后创建 commit 对象

对象存储

之前我提到当存储数据内容时,同时会有一个文件头被存储起来。我们花些时间来看看 Git 是如何存储对象的。你将看来如何通过 Ruby 脚本语言存储一个 blob 对象 (这里以字符串 “what is up, doc?” 为例) 。使用 irb 命令进入 Ruby 交互式模式:

9.3 Git References

你可以执行像 git log 1a410e 这样的命令来查看完整的历史,但是这样你就要记得 1a410e 是你最后一次提交,这样才能在提交历史中找到这些对象。你需要一个文件来用一个简单的名字来记录这些 SHA-1 值,这样你就可以用这些指针而不是原来的 SHA-1 值去检索了。

每当你执行 git branch (分支名称) 这样的命令,Git 基本上就是执行 update-ref 命令,把你现在所在分支中最后一次提交的 SHA-1 值,添加到你要创建的分支的引用。

HEAD 标记

现在的问题是,当你执行 git branch (分支名称) 这条命令的时候,Git 怎么知道最后一次提交的 SHA-1 值呢?答案就是 HEAD 文件。HEAD 文件是一个指向你当前所在分支的引用标识符。这样的引用标识符——它看起来并不像一个普通的引用——其实并不包含 SHA-1 值,而是一个指向另外一个引用的指针。如果你看一下这个文件,通常你将会看到这样的内容:

当你创建一个新的分支时:

.git/HEAD 文件的路径发生变化,并且 .git/refs 目录下出现一个新的文件记录master分支上最近一次提交的SHA值。

Tags

你刚刚已经重温过了 Git 的三个主要对象类型,现在这是第四种。Tag 对象非常像一个 commit 对象——包含一个标签,一组数据,一个消息和一个指针。最主要的区别就是 Tag 对象指向一个 commit 而不是一个 tree。它就像是一个分支引用,但是不会变化——永远指向同一个 commit,仅仅是提供一个更加友好的名字。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-c1Ilpsfx-1586850656660)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20200414151316884.png)]

Remotes

你将会看到的第四种 reference 是 remote reference

9.4 Packfiles

事实上 Git 可以那样做。Git 往磁盘保存对象时默认使用的格式叫松散对象 (loose object) 格式。Git 时不时地将这些对象打包至一个叫 packfile 的二进制文件以节省空间并提高效率。当仓库中有太多的松散对象,或是手工调用 git gc 命令,或推送至远程服务器时,Git 都会这样做。

9.5 The Refspec

推送 Refspec

采用命名空间的方式确实很棒,但QA组成员第1次是如何将他们的分支推送到 qa/ 空间里面的呢?答案是你可以使用 refspec 来推送。

如果QA组成员想把他们的 master 分支推送到远程的 qa/master 分支上,可以这样运行:

$ git push origin master:refs/heads/qa/master

如果他们想让 Git 每次运行 git push origin 时都这样自动推送,他们可以在配置文件中添加 push 值:

[remote "origin"]
    url = git@github.com:schacon/simplegit-progit.git
    fetch = +refs/heads/*:refs/remotes/origin/*
    push = refs/heads/master:refs/heads/qa/master

这样,就会让 git push origin 缺省就把本地的 master 分支推送到远程的 qa/master 分支上。

删除引用

你也可以使用 refspec 来删除远程的引用,是通过运行这样的命令:

$ git push origin :topic

因为 refspec 的格式是 :, 通过把 `` 部分留空的方式,这个意思是是把远程的 topic 分支变成空,也就是删除它。

9.6 传输协议

Git 可以以两种主要的方式跨越两个仓库传输数据:基于HTTP协议之上,和 file://, ssh://, 和 git:// 等智能传输协议。这一节带你快速浏览这两种主要的协议操作过程。

哑协议

Git 基于HTTP之上传输通常被称为哑协议,这是因为它在服务端不需要有针对 Git 特有的代码。这个获取过程仅仅是一系列GET请求,客户端可以假定服务端的Git仓库中的布局。

智能协议

这个HTTP方法是很简单但效率不是很高。使用智能协议是传送数据的更常用的方法。这些协议在远端都有Git智能型进程在服务 - 它可以读出本地数据并计算出客户端所需要的,并生成合适的数据给它,这有两类传输数据的进程:一对用于上传数据和一对用于下载。

上传数据

为了上传数据至远端, Git 使用 send-packreceive-pack 进程。这个 send-pack 进程运行在客户端上,它连接至远端运行的 receive-pack 进程。

下载数据

当你在下载数据时, fetch-packupload-pack 进程就起作用了。客户端启动 fetch-pack 进程,连接至远端的 upload-pack 进程,以协商后续数据传输过程。

9.7 维护及数据恢复

你时不时的需要进行一些清理工作 ── 如减小一个仓库的大小,清理导入的库,或是恢复丢失的数据。本节将描述这类使用场景。

维护

Git 会不定时地自动运行称为 “auto gc” 的命令。大部分情况下该命令什么都不处理。不过要是存在太多松散对象 (loose object, 不在 packfile 中的对象) 或 packfile,Git 会进行调用 git gc 命令。 gc 指垃圾收集 (garbage collect),此命令会做很多工作:收集所有松散对象并将它们存入 packfile,合并这些 packfile 进一个大的 packfile,然后将不被任何 commit 引用并且已存在一段时间 (数月) 的对象删除。

可以手工运行 auto gc 命令:

$ git gc --auto

数据恢复

在使用 Git 的过程中,有时会不小心丢失 commit 信息。这一般出现在以下情况下:强制删除了一个分支而后又想重新使用这个分支,hard-reset 了一个分支从而丢弃了分支的部分 commit。如果这真的发生了,有什么办法把丢失的 commit 找回来呢?

下面的示例演示了对 test 仓库主分支进行 hard-reset 到一个老版本的 commit 的操作,然后恢复丢失的 commit 。首先查看一下当前的仓库状态:

$ git log --pretty=oneline
    ab1afef80fac8e34258ff41fc1b867c702daa24b modified repo a bit
    484a59275031909e19aadb7c92262719cfcdf19a added repo.rb
    1a410efbd13591db07496601ebc7a059dd55cfe9 third commit
    cac0cab538b970a37ea1e769cbbde608743bc96d second commit
    fdf4fc3344e67ab068f836878b6c4951e3b15f3d first commit

接着将 master 分支移回至中间的一个 commit:

$ git reset --hard 1a410efbd13591db07496601ebc7a059dd55cfe9
    HEAD is now at 1a410ef third commit
    $ git log --pretty=oneline
    1a410efbd13591db07496601ebc7a059dd55cfe9 third commit
    cac0cab538b970a37ea1e769cbbde608743bc96d second commit
    fdf4fc3344e67ab068f836878b6c4951e3b15f3d first commit

这样就丢弃了最新的两个 commit ── 包含这两个 commit 的分支不存在了。

移除对象

对仓库进行 gc 操作,并查看占用了空间:

$ git gc
    Counting objects: 21, done.
    Delta compression using 2 threads.
    Compressing objects: 100% (16/16), done.
    Writing objects: 100% (21/21), done.
    Total 21 (delta 3), reused 15 (delta 1)

可以运行 count-objects 以查看使用了多少空间:

$ git count-objects -v
    count: 4
    size: 16
    in-pack: 21
    packs: 1
    size-pack: 2016
    prune-packable: 0
    garbage: 0

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZxwdVb9u-1586850656661)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20200414154923333.png)]

size-pack 是以千字节为单位表示的 packfiles 的大小,因此已经使用了 2MB 。而在这次提交之前仅用了 2K 左右 ── 显然在这次提交时删除文件并没有真正将其从历史记录中删除。

sed 15 (delta 1)


可以运行 `count-objects` 以查看使用了多少空间:

$ git count-objects -v
count: 4
size: 16
in-pack: 21
packs: 1
size-pack: 2016
prune-packable: 0
garbage: 0


[外链图片转存中...(img-ZxwdVb9u-1586850656661)]

`size-pack` 是以千字节为单位表示的 packfiles 的大小,因此已经使用了 2MB 。而在这次提交之前仅用了 2K 左右 ── 显然在这次提交时删除文件并没有真正将其从历史记录中删除。

运行另一个底层命令 `git verify-pack` 以识别出大对象,对输出的第三列信息即文件大小进行排序,还可以将输出定向到 `tail` 命令,因为你只关心排在最后的那几个最大的文件:
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值