在团队开发中,我们每天都在敲 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 命令的时候。
的 object 是没必要的,git 自然也做了处理,就是 git gc 命令,它会把所有 object 打包到一起,并且类似的内容只会保留最新的那个,其余的只保存 diff。
其实你每天都能看到 git gc 的身影,尤其是你执行 git clone、git push 命令的时候。
下次再 clone、push,你能想起这个 git gc 了么?