Git深入理解
Git是我们平时团队协作必不可少的版本控制系统。这次就来深入探讨一下其中的原理。
commit
我们先新建一个项目,就叫TestGit
吧。。。我们先来看一下目录结构:
然后我们用tree
命令看一下目录结构
然后我们初始化一下git:
先初始化git,然后添加所有文件到git的暂存区,成功后提交一个commit,最后log一下版本历史记录
,大概就是进行了这几个步骤。
那么这个commit到底执行的是什么操作呢?
先别急,我们先来看看着张图:
这是项目的三个版本,版本1中有两个文件A和B,然后修改了A,变成了A1,形成了版本2,接着又修改了B变为B1,形成了版本3。
如果我们把项目的每个版本都保存到本地仓库,需要保存至少6个文件,而实际上,只有4个不同的文件,A、A1、B、B1。为了节省存储的空间,我们要想一个方法将同样的文件只需要保存一份,这就引入了Sha-1算法。
SHA-1将文件中的内容通过通过计算生成一个 40 位长度的hash值。
Sha-1特点:
- 由文件内容计算出的hash值
- hash值相同,文件内容相同
对于上图中的内容,无论我们执行多少次,都会得到相同的结果。因此,文件的sha-1值是可以作为文件的唯一 id ,同时,它还有一个额外的功能,校验文件完整性。
其实当我们运行git commit -m "git init"
命令时,git主要进行了三个操作:
1.为每一个文件生成一个快照
每一个文件其实是真的数据,所以git会把整个文件内容转成二进制,然后经过压缩直接存在键值对数据库中,对应的键值就是文件中的内容再附加一些头信息的40位校验和sha-1。既然是真数据,所以文件快照的类型为blob
类型(binary large object)即大型二进制对象类型blob: 用来存放项目文件的内容,但是不包括文件的路径、名字、格式等其它描述信息。项目的任意文件的任意版本都是以blob的形式存放的。
2.为每一个文件夹生成一个快照
文件夹并不是直接的文字数据,其主要记录的是文件夹的结构和每个文件或者文件夹所对应的快照键值,所以文件夹的快照内容主要是其包含的所有文件和文件夹的键值信息总和,附加一些头信息,如文件名,文件夹名。对应快照键值为快照内容的40位校验和sha-1。既然不是直接数据,数据类型与文件快照必然不同,文件夹快照对应的类型为tree类型。tree :用来表示目录。我们知道项目就是一个目录,目录中有文件、有子目录。因此 tree 中有 blob、子tree,且都是使用 sha-1值引用的。这是与目录对应的。从顶层的 tree 纵览整个树状的结构,叶子结点就是blob,表示文件的内容,非叶子结点表示项目的目录,顶层的 tree 对象就代表了当前项目的快照。
3.生成一个项目快照
也即生成一个commit,项目快照的内容主要包含四部分信息,根项目目录的快照、提交人信息、项目快照说明(即commit信息)和父项目快照。其中项目文件快照,只要根目录即’TestGit’的目录快照即可。项目快照commit的键值为项目快照内容的40位校验和sha-1。项目快照类型为commit类型。commit: 表示一次提交,有parent字段,用来引用父提交。指向了一个顶层 tree,表示了项目的快照,还有一些其它的信息,比如上一个提交,committer、author、message 等信息。
我们可以看下数据库中的文件
我们可以看到,在 objects 目录下,存放了很多文件,他们都使用 sha-1 的前两位创建了文件夹,剩下的38位为文件名。我们先称呼这些文件为 obj 文件。
对于这么多的 obj 文件,就保存了我们代码提交的所有记录。对于这些 obj 文件,其实分为四种类型,分别是 blob、tree、commit、tag。
我们可以使用命令 find .git/objects -type f
看到每一个object的信息
通过 cat-file
命令可以将数据内容取回, 传入 -p
参数可以让该命令输出数据内容的类型:
这些object其实就是这些文件
如果我们添加了新的文件,其所在的文件夹也因新添加的文件而产生了内容的变化,因此也会生成新的快照,同样的根文件夹TestGit
也生成了新的快照。最终的形成新的commitd指向最新的根文件TestGit
的快照。只有变化的文件或文件夹才会形成新的快照,没有变化的文件不会形成新的快照。
branch-分支
分支的目的是让我们可以并行的进行开发。比如我们当前正在开发功能,但是需要修复一个紧急bug,我们不可能在这个项目正在修改的状态下修复 bug,因为这样会引入更多的bug。有了分支的概念,我们就可以新建一个分支,修复 bug,使新功能与 bug 修复同步进行。
分支的实现其实很简单,我们可以先看一下 .git/refs/heads/master
文件,它保存了当前的分支。
打开master,里面的内容是bdac9e2702227bd5a2f5024742e42a6cd4636553
,这是最新commit的键值,所以branch仅仅是指向一个commit的指针而已,指向一个commit,而一个commit同时指向其父commit,如此循环最终形成了一个branch
head指针
那git是怎么知道项目在master分支上呢?HEAD指针。git有一个独立的HEAD指针,记录项目现在所在的位置,比如现在我们在master分支上,查看.git/HEAD
文件,内容为:ref: refs/heads/master
此时HEAD指针指向master,所以项目在master分支上
当我们创建一个新的分支test时,git会在.git/refs/heads/目录下生成一个文件test,并将其指向当前HEAD所指向的分支master所指向的提交,并把HEAD指向新的分支test(ref: refs/heads/test
)。
当我们在新的分支生成新的commit时,git会将HEAD所指向的分支test所指向的commit作为新commit的父commit,然后将HEAD所指向的分支test移动指向新的提交。
总结
git中每一个文件会生成一个blob类型的object记录这个文件的状态,每一个文件夹都会生成一个tree类型的object就录这个文件夹的状态,一个项目会生成一个指向根目录tree object的commit类型object作为项目的快照。
branch其实只是指向一个commit的指针而已,HEAD记录了当前项目的位置。
最后更新: 2023年03月25日 22:39:55
本文链接: http://aeronxie.github.io/post/3cd14bc1.html
版权声明: 本作品采用 CC BY-NC-SA 4.0 许可协议进行许可,转载请注明出处!