Git是如何管理版本文件的
0. 摘要
git是每一个程序员必会的工具,也是我们面临的一大挑战:多少初入行的新手被挡在门外,多少团队矛盾是由不得当的git操作导致。所以,我将花很长的时间来总结一系列的文章内容来带你更深入了解git。
作为系列的第一篇,我并不会介绍Git 的基本命令操作(git init, add, commit,checkout)而是重点来讨论git到底是如何记录我们文件的变化的。试图去回答:文件的历史版本被记录在了哪里?它们是如何切换的?
1. 结论
Git做版本管理的方式是直接记录文件快照。
Git 是把变化的文件作快照(可以理解为对文件进行压缩处理之后得到的副本)后,记录在一个微型的文件系统 (可以理解为.git目录下的objects文件夹)中。每次提交更新时,它会纵览一遍所有文件的指纹信息并对文件作一快照,然后保存一个指向这次快照的索引。
举个例子哈:
你的项目中有一个文件: big.txt,它目前有100k,在GIT的文件系统中有一个快照是88k。
如果当前你修改了文件(比如就是在文件的末尾添加一些代码),让文件变成了101K,然后做了一次 git add big.txt,那么 git中将对这个101K的新文件,重新拍一个快照(可能这个快照文件的大小可能变成了89K),然后再保存在GIT的文件系统中。
当然了,为提高性能,若文件没有变化,Git 不会再次保存,而只对上次保存的快照作一链接。
上图中A,B,C三个文件经历了5个版本的变化。
从版本2变化到版本3时,文件A,B没有变化,而C从C1变化到C2。所以本次快照只拍了C2.
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。
3. add做了什么事
接下来,我们在这个空项目下 添加一个文件: test.txt,内容是version1 (当然你可以改成其他的)
然后运行:git add test.txt,我们知道add名字会让文件进入暂存区,那到底在物理文件上发生了什么事呢?
上面的名字之后,test.txt就被加入了git的暂存区。e3
这个文件夹,就是本次操作的新添加的文件夹,同时,在它的下面有一个长度为38个字符的文件。用记事本打开这个文件,内容如下:
注意,如果你的test.txt的文件内容和我的操作不一样,那你看到的就可能不是叫e3这个名字了。
好了,到了这里,我们慢慢逼近了问题:
这个莫名的
e3
和2092a83f837140c08e85a60ef16a6b2a208986
是什么?记事本打开的这个内容又是什么?
他们和我刚才创建的文件test.txt有什么关系?
来,直接看答案:
e3及那个38位的文件名是 Git 存储数据内容的方式──为每份内容生成一个文件,取得该内容与头信息的 SHA-1 校验和,创建以该校验和前两个字符为名称的子目录,并以 (校验和) 剩下 38 个字符为文件命名 (保存至子目录下)。如果用一个公式的话:
SHA-1是一种算法,这里是百度百科:https://baike.baidu.com/item/SHA-1/1699692?fr=aladdin
拥有38位长度的文件
中的内容是对test.txt的内容进行了zlib 压缩之后的结果。zlib是一种压缩算法,这里是百度百科:https://baike.baidu.com/item/zlib。我们可以通过一个神奇的命令cat-file来查看这个文件的内容:
git cat-file -p e32092a83f837140c08e85a60ef16a6b2a208986
# e3是文件夹名, 后面的一串是文件名
# cat-file命令 用来查看显示版本库对象的内容、类型及大小信息。
# -p 根据对象的类型以优雅的方式显式对象内容
执行结果如下:
结论:add 将文件进行快照(zlib压缩),然后建立索引(内容与头信息的 SHA-1 校验和),保存到文件系统中(校验和的前两位是文件夹名,其他38位是文件名)
来看commit操作。
4. git commit 做了什么事
在上面操作的基础上,接着做一次git commit
会发现objects文件夹下会多出 1b, ff 这两个文件夹,各自的下边又有一个文件:
下面继续用cat-file命令来查看内容。
先看ff下的内容:这是一个提交明细(可以理解为购买商品时的具体明细列表)
git cat-file -p ffe9ce5421c3a1cbd84a858f8f5696029574abdc
100644 blob e32092a83f837140c08e85a60ef16a6b2a208986 test.txt
文件模式为
100644
,表明这是一个普通文件。blob 是计算机术语 ,通常理解为文件。
e32092a83f837140c08e85a60ef16a6b2a208986 就是本次保存下来的文件的快照。
再看1b
下那个文件的内容:这是一个提交记录(可以理解为商品时的流水号)
git cat-file -p 1b131794d454360758ff17313aa77c17e1423aaa
tree ffe9ce5421c3a1cbd84a858f8f5696029574abdc
author fanyoufu <[email protected]> 1626018103 +0800
committer fanyoufu <[email protected]> 1626018103 +0800
这是我们做的第一次提交
commit 格式:指明了该时间点项目快照的顶层树对象、作者/提交者信息(从 Git 设置的 user.name
和 user.email
中获得)以及当前时间戳、一个空行,以及提交注释信息。
此时通过git log
命令也可以看到 我们的提交信息了:
git log
commit 1b131794d454360758ff17313aa77c17e1423aaa (HEAD -> master)
Author: fanyoufu <[email protected]>
Date: Sun Jul 11 23:41:43 2021 +0800
这是我们做的第一次提交
同时,.git/logs/refs/heads/master文件也有变化。
结论:commit之后做的事情:
生成commitID,保存commit信息
生成commit明细,记录本次提交具体涉及哪些文件
推动heads指针移动,并保存日志。
5. 进一步修改文件内容并提交
继续模拟日常操作。
添加一个目录 lib,下边放一个文件a.txt,文件内容也是'a.txt'
将test.txt的内容从'version1'改成一个比较大的文件内容(建议几百KB,后边好看效果。例如:抄某个框架、库的源码)
运行:git add .
之后,多出两个文件夹
具体的文件如下:
显然,这两个就是刚才对a.txt和test.txt的快照文件了。(86
目录下的文件有110KB,显然就是对原348KB的压缩处理)
类似的,我们可以通过
git cat-file -p 86e6a05b231f0317de4c3c9d1072c63738d7af95
就可以看到当前test.txt的内容了。
继续,git commit 之后
分别用 git cat-file -p 来查看
log的变化
6. 查看某一次提交时的某个文件的完整内容
查看某一次提交时的某个文件的完整内容:
git show 提交日志ID:文件名 # 有个冒号
上面已经有现成的命令了,如果要抖个机灵 用cat-file怎么操作呢,具体如下:
git log 文件名 # 查看提交记录
git cat-file -p commitID # 查看commit明细ID
git cat-file -p commit明细ID # 查看具体的文件快照ID
git cat-file -p 文件快照ID 得到结果
这里有个具体的示例
7. git branch 做了什么事?
创建分支git branch dev
,就是在heads下新建一个dev文件,内容和master文件的内容一样:就是最后一次commit的commitID。
是不是好快~~
8. git checkout 切换分支做了什么事?
就是修改 .git下HEAD文件的内容。
是不是好快~~
9. 保存快照的缺陷
先说结论:只要文件变化了,并且add了,就会有快照,就会有空间浪费
修改给大文件test.txt中稍作修改:
删除全部内容,写入!!!!,然后之间git add .(注意,没有commit , 只要文件变化了,并且add了,就会有快照)
删除上边的4个!!!!,再写入一个!,然后再次git add . (注意,没有commit ,只要文件变化了,并且add了,就会有快照,虽然没有提交)
还原之前的大文件内容,然后任意额外补充一句代码(在348K的test.txt中,补充一个注释)。并git add test.txt ,git commit 第三个版本。
上面的1,2步操作的影响会在最后揭晓。
此时查看日志,有了一个全新的提交记录
然后,不出意外的,test.txt文件再次有了一个巨大的快照:在59这个目录下边,经过zlip压缩,这个test.txt的大小是111KB。
现在比较坑呀:在仓库中保存了两次大文件,首次保存是 110KB,然后加了一句代码,再commit一次 又占了111KB。其实这110KB和111KB之间,几乎一样!是不是很浪费。
10.用git gc 命令清理优化
好,现在运行:
git gc
#清理不必要的文件并优化本地存储库
会发现objects下的东西少了很多,同时 整个.git文件夹的大小也大大减少了。
文件内容变化具体如下:
同时,在info下也多了一个文件packs,内容
查看上面的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 命令查看文件快照的内容。
如果对你有帮助,欢迎转发,点赞,关注。