这个 git 命令你每天都在用,但你却不知道

9,333 阅读4分钟

我们每天都在敲 git clone、add、commit、push 这些命令,其实它们涉及到一个底层命令。

这个命令你每天都在用,每天都能看到它的输出,但你却不知道你用到了这个命令。

那这个命令是啥呢?

git 存储内容是通过 object 的形式,文件内容是 blob 的 object,目录是 tree 的 object,commit 就是 commit 的 object。

这三种 object 串联起来就是 git 存储的内容了。

然后 branch 和 tag 都是指向 commit 对象的指针。

也就是这样:

这就是 git 的存储原理。

在 .git 目录下可以看到所有这些 object:

你 git clone 和 push 的时候,其实也就是下载 object:

但修改同一个文件,只是改了一点也会创建一个新的 object,因为 hash 变了。

如果整个大文件就改了一点东西,却把整体都保存为一个新的 object,岂不是太浪费空间了?

没错,git 自然也想到了这点,所以它会做压缩。

怎么压缩的呢?

你可以执行 git gc 这个命令来体验下压缩过程。

比如 text.txt 这个文件,我先 git commit 一次。

然后把最后一行改为 bbb,git commit 一次。再改为 ccc 再 git commit 一次。

这时候有 9 个 object:

因为 3 次内容变动会有 3 个 blob 类型的 object,然后有 3 个 tree object 来指向它们,还有 3 个 commit object。

比如其中一个 tree object 的内容是这样的:

(cat-file -p 就是查看对象内容的命令)

它指向一个 blob 对象。blob 对象内容是这样的:

而这个 tree 对象也有 commit 对象指向它:

这就是 3 种对象的关系。

我们跑下 git gc 看看会发生什么:

输出内容如上,是不是有种莫名的熟悉感?

为什么有熟悉感待会再说,我们先来看看它都做了什么。

你会发现执行完 gc 后 objects 下都没有那些对象了,但是在 pack 目录下多了一个 idx 文件和一个 pack 文件。

这就是压缩打包后的结果。

咋压缩的呢?

看下它的内容就知道了:

执行 git verify-pack -v 看下 idx 文件的内容:

你会看到 3 个 commit、3 个 blob、3 个 tree 对象都列出来了,而且 3 个 blob 对象之间还有指向关系,也就是这个:

叫做 chain。

这是啥呢?

其实 pack 里就是打包后的 object,然后 idx 记录着不同文件在其中的位置。

但是总不能把同样的文件原封不动保存多份吧。

所以 git 会对内容相近的文件做 diff,只保留一个版本的全部内容,其余的都是保存 diff。

那保存哪份呢?

用 cat-file -p 看看它的内容:

发现是保存的最后那个版本的全部内容,之前的版本保存 diff。

原因很容易想到,最新的肯定用的最频繁嘛,这样处理起来也方便。

这就是 git gc 压缩的原理(gc 是 garbage collection,垃圾回收的意思)。

那这个命令和 git clone、git push 有啥关系呢?

你再瞅瞅它的输出看看:

是不是很熟悉?

对比下 git push 的输出:

是不是一毛一样!

没错,在 git push 之前,git 会执行 git gc 来压缩打包再传输。

再来看看 git clone 的输出:

同样能看到 git gc 的身影。

没错,在 git clone 的时候,服务端也会执行 git gc 再传输。

所以 git gc 这个命令你每天都在用,每天都能看到它的输出,但你却不知道它的存在。

总结

git 通过 blob 存储文件内容,tree 存储目录信息,commit 存储提交信息,这 3 种对象关联起来就是 git 的存储原理。

但是同样的内容保存多个类似的 object 是没必要的,git 自然也做了处理,就是 git gc 命令,它会把所有 object 打包到一起,并且类似的内容只会保留最新的那个,其余的只保存 diff。

其实你每天都能看到 git gc 的身影,尤其是你执行 git clone、git push 命令的时候。

下次再 clone、push,你能想起这个 git gc 了么?