vlambda博客
学习文章列表

Git是如何管理版本文件的

0. 摘要

git是每一个程序员必会的工具,也是我们面临的一大挑战:多少初入行的新手被挡在门外,多少团队矛盾是由不得当的git操作导致。所以,我将花很长的时间来总结一系列的文章内容来带你更深入了解git。


作为系列的第一篇,我并不会介绍Git 的基本命令操作(git init, add, commit,checkout)而是重点来讨论git到底是如何记录我们文件的变化的。试图去回答:文件的历史版本被记录在了哪里?它们是如何切换的?


1. 结论


  1. Git做版本管理的方式是直接记录文件快照。

Git 是把变化的文件作快照(可以理解为对文件进行压缩处理之后得到的副本)后,记录在一个微型的文件系统 (可以理解为.git目录下的objects文件夹)中。每次提交更新时,它会纵览一遍所有文件的指纹信息并对文件作一快照,然后保存一个指向这次快照的索引。


举个例子哈:

你的项目中有一个文件: big.txt,它目前有100k,在GIT的文件系统中有一个快照是88k。

如果当前你修改了文件(比如就是在文件的末尾添加一些代码),让文件变成了101K,然后做了一次 git add big.txt,那么 git中将对这个101K的新文件,重新拍一个快照(可能这个快照文件的大小可能变成了89K),然后再保存在GIT的文件系统中。

Git是如何管理版本文件的



当然了,为提高性能,若文件没有变化,Git 不会再次保存,而只对上次保存的快照作一链接。

Git是如何管理版本文件的

上图中A,B,C三个文件经历了5个版本的变化。

从版本2变化到版本3时,文件A,B没有变化,而C从C1变化到C2。所以本次快照只拍了C2.



  1. Git提供的gc命令,可以优化第1条带来的空间浪费的问题。



好了,结论说完了,我们来开始动手啦。边所有的操作在:GIT 2.21.0 + Win10 下进行。


2. 初始化命令做了什么事


我们在d:\learn-git下,运行命令 git init之后,会在learn-git下创建一个.git目录,这个目录默认是隐藏的,你需要根据你自己的操作系统不同,去设置让它显示出来。


.git文件夹就是我们工作的起点,它基本结构和内容如下:

.git  |- hooks        |-applypatch-msg.sample # hooks钩子函数        |-xxxxxxxx.sample  |- info        |-exclude  #不希望在.gitignore 文件中管理的忽略模式 (ignored patterns) 的全局可执行文件  |- objects # 所有的数据内容        |-info        |-pack  |- refs #目录存储指向数据 (分支) 的提交对象的指针        |-heads #文件指向当前分支        |-tags  |- config  # 包含项目的特有配置选项  |- description  |HEAD: 内容ref: refs/heads/master


请注意 .git/objects目录,它是我们关注的重点。


我建议你在电脑上跟着操作,像我这样将屏幕分屏:一边是git bash,另一边是.git/object。

Git是如何管理版本文件的

3. add做了什么事


接下来,我们在这个空项目下 添加一个文件: test.txt,内容是version1  (当然你可以改成其他的)

Git是如何管理版本文件的

然后运行:git add test.txt,我们知道add名字会让文件进入暂存区,那到底在物理文件上发生了什么事呢?


Git是如何管理版本文件的

上面的名字之后,test.txt就被加入了git的暂存区。e3这个文件夹,就是本次操作的新添加的文件夹,同时,在它的下面有一个长度为38个字符的文件。用记事本打开这个文件,内容如下:

Git是如何管理版本文件的

注意,如果你的test.txt的文件内容和我的操作不一样,那你看到的就可能不是叫e3这个名字了。


好了,到了这里,我们慢慢逼近了问题:

  1. 这个莫名的e32092a83f837140c08e85a60ef16a6b2a208986 是什么?

  2. 记事本打开的这个内容又是什么?

  3. 他们和我刚才创建的文件test.txt有什么关系?


来,直接看答案:

  1. e3及那个38位的文件名是 Git 存储数据内容的方式──为每份内容生成一个文件,取得该内容与头信息的 SHA-1 校验和,创建以该校验和前两个字符为名称的子目录,并以 (校验和) 剩下 38 个字符为文件命名 (保存至子目录下)。如果用一个公式的话:

Git是如何管理版本文件的

SHA-1是一种算法,这里是百度百科:https://baike.baidu.com/item/SHA-1/1699692?fr=aladdin


  1. 拥有38位长度的文件中的内容是对test.txt的内容进行了zlib 压缩之后的结果。zlib是一种压缩算法,这里是百度百科:https://baike.baidu.com/item/zlib。我们可以通过一个神奇的命令cat-file来查看这个文件的内容:

git cat-file -p e32092a83f837140c08e85a60ef16a6b2a208986 # e3是文件夹名, 后面的一串是文件名# cat-file命令 用来查看显示版本库对象的内容、类型及大小信息。# -p 根据对象的类型以优雅的方式显式对象内容


执行结果如下:


Git是如何管理版本文件的


结论:add 将文件进行快照(zlib压缩),然后建立索引(内容与头信息的 SHA-1 校验和),保存到文件系统中(校验和的前两位是文件夹名,其他38位是文件名)


来看commit操作。

4. git commit 做了什么事

在上面操作的基础上,接着做一次git commit


Git是如何管理版本文件的


会发现objects文件夹下会多出 1b, ff 这两个文件夹,各自的下边又有一个文件:

Git是如何管理版本文件的

下面继续用cat-file命令来查看内容。

先看ff下的内容:这是一个提交明细(可以理解为购买商品时的具体明细列表)

$ git cat-file -p ffe9ce5421c3a1cbd84a858f8f5696029574abdc100644 blob e32092a83f837140c08e85a60ef16a6b2a208986 test.txt
  • 文件模式为 100644表明这是一个普通文件。

  • blob 是计算机术语 ,通常理解为文件。

  • e32092a83f837140c08e85a60ef16a6b2a208986 就是本次保存下来的文件的快照。

再看1b下那个文件的内容:这是一个提交记录(可以理解为商品时的流水号)

$ git cat-file -p 1b131794d454360758ff17313aa77c17e1423aaatree ffe9ce5421c3a1cbd84a858f8f5696029574abdcauthor fanyoufu <[email protected]> 1626018103 +0800committer fanyoufu <[email protected]> 1626018103 +0800这是我们做的第一次提交

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


此时通过git log命令也可以看到 我们的提交信息了:

git logcommit 1b131794d454360758ff17313aa77c17e1423aaa (HEAD -> master)Author: fanyoufu <[email protected]>Date: Sun Jul 11 23:41:43 2021 +0800 这是我们做的第一次提交


同时,.git/logs/refs/heads/master文件也有变化。

Git是如何管理版本文件的


Git是如何管理版本文件的



结论:commit之后做的事情:

  1. 生成commitID,保存commit信息

  2. 生成commit明细,记录本次提交具体涉及哪些文件

  3. 推动heads指针移动,并保存日志。



5. 进一步修改文件内容并提交


继续模拟日常操作。


  1. 添加一个目录 lib,下边放一个文件a.txt,文件内容也是'a.txt'

  2. 将test.txt的内容从'version1'改成一个比较大的文件内容(建议几百KB,后边好看效果。例如:抄某个框架、库的源码)

Git是如何管理版本文件的


运行:git add . 之后,多出两个文件夹

Git是如何管理版本文件的


具体的文件如下:

Git是如何管理版本文件的

显然,这两个就是刚才对a.txt和test.txt的快照文件了。(86目录下的文件有110KB,显然就是对原348KB的压缩处理)


类似的,我们可以通过

git cat-file -p 86e6a05b231f0317de4c3c9d1072c63738d7af95 就可以看到当前test.txt的内容了。



继续,git commit 之后

Git是如何管理版本文件的


分别用 git cat-file -p 来查看

Git是如何管理版本文件的

Git是如何管理版本文件的

Git是如何管理版本文件的


log的变化

Git是如何管理版本文件的


6. 查看某一次提交时的某个文件的完整内容


查看某一次提交时的某个文件的完整内容:

git show 提交日志ID:文件名 # 有个冒号

上面已经有现成的命令了,如果要抖个机灵 用cat-file怎么操作呢,具体如下:


git log 文件名 # 查看提交记录git cat-file -p commitID # 查看commit明细IDgit cat-file -p commit明细ID # 查看具体的文件快照IDgit cat-file -p 文件快照ID 得到结果


这里有个具体的示例

Git是如何管理版本文件的

7. git branch 做了什么事?


创建分支git branch dev,就是在heads下新建一个dev文件,内容和master文件的内容一样:就是最后一次commit的commitID。

Git是如何管理版本文件的

是不是好快~~

8. git checkout 切换分支做了什么事?


就是修改 .git下HEAD文件的内容。

Git是如何管理版本文件的


是不是好快~~

9. 保存快照的缺陷


先说结论:只要文件变化了,并且add了,就会有快照,就会有空间浪费


修改给大文件test.txt中稍作修改:

  1. 删除全部内容,写入!!!!,然后之间git add .(注意,没有commit , 只要文件变化了,并且add了,就会有快照)

  2. 删除上边的4个!!!!,再写入一个!,然后再次git add . (注意,没有commit ,只要文件变化了,并且add了,就会有快照,虽然没有提交)

  3. 还原之前的大文件内容,然后任意额外补充一句代码(在348K的test.txt中,补充一个注释)。并git add test.txt ,git commit 第三个版本。


上面的1,2步操作的影响会在最后揭晓。


此时查看日志,有了一个全新的提交记录


Git是如何管理版本文件的


然后,不出意外的,test.txt文件再次有了一个巨大的快照:在59这个目录下边,经过zlip压缩,这个test.txt的大小是111KB。

Git是如何管理版本文件的



现在比较坑呀:在仓库中保存了两次大文件,首次保存是 110KB,然后加了一句代码,再commit一次 又占了111KB。其实这110KB和111KB之间,几乎一样!是不是很浪费。



10.用git gc 命令清理优化

好,现在运行:

git gc#清理不必要的文件并优化本地存储库

会发现objects下的东西少了很多,同时 整个.git文件夹的大小也大大减少了。


文件内容变化具体如下:

Git是如何管理版本文件的


同时,在info下也多了一个文件packs,内容

Git是如何管理版本文件的


查看上面的ab和4e的内容:

就是没有被任何 commit 引用的 blob :你从没将他们添加至任何 commit,所以 Git 认为它们是 "悬空" 的,不会将它们打包进 packfile 。它们就是我们在前面做的两次没有commit的add。


再来通过git verify-pack -v xxxxxxx.idx 来查看具体的打包细节,如下:


如果你还记得的话, 86e6这个 blob 是 test.txt 文件的第二个版本(大文件),这个 blob 引用了 592d这个 blob,即该文件的第三个版本(大文件加了一行代码)。命令输出内容的第三列显示的是对象大小,可以看到 592d占用了 300多K 空间,而 86e6仅为 25 字节:   也就是说最新的版本才是完整保存文件内容的对象,而第二个版本是以差异方式保存的 ── 这是因为大部分情况下需要快速访问文件的最新版本。


你还可以通过另一命令git count-objects 可以来计算有多少个git对象。如果这个值特别大,那就可以考虑使用 git gc 来清理啦。具体是多大的量化指标呢,其实也没有定论,如果你有参考答案,欢迎你告诉我。


小结

本文解释了一个结论:GIT其实是通过直接记录快照这个粗暴,有效,费空间的方式来做版本管理的。但是,它提供了gc命令来做优化(差异化的方式保存)。同时,我们深入到了.git文件夹中,观察了init, add, commit,checkout 等命令对文件内容的响应,用 cat-file 命令查看文件快照的内容。


如果对你有帮助,欢迎转发,点赞,关注。