Git:从实践到原理,再到实践
写在前面
在如今的时代,相信没有人还不会用Git了吧。虽然Git只是用两周时间开发出来的,但因其优秀的性能、简洁的操作,已经成为了使用最广泛的版本管理系统。
在与工作的过程中发现,大家对Git的使用等级,大致分为3种。
①,把Git当成svn使用,有分支的概念,但无法熟练使用
②,可以熟练使用git,可以应付大部分的Git操作,但无法灵活的操作,发生操作失误时不知道怎么办
③,基本可以解决项目上所有的git问题,熟练使用一些冷门但实用的命令(reflog
、rebase
、cherry-pick
等)
对于一个成熟的程序员来讲,如果你还是①的等级,说明你需要学一下Git操作了,否则被市场淘汰只是时间问题了。 如果你达到了②的程度,Git就不再限制你的开发效率,但无法承担起核心的责任。 而如果达到③的程度的话就足以独当一面了,可以自己去设计一套项目的开发流程、分支结构。 但是,真正了解Git的运行、存储原理的人其实非常少,而了解这些会帮助我们更加高效优雅的使用Git。 本篇内容并不适合等级①的开发者阅读,建议先学习一下Git的基础知识后再探究其原理。
起因
在一个适(huo)合(gan)学(wan)习(le)的工作日,沉浸于Git操作的我突然想到,以前学习Git时学到,Git的存储形式不同于SVN的Diff存储,而是将文件以SNAPSHOT(快照)
的形式存储,以空间换时间的方式提高效率。
而在实际使用过程中,并没有发现项目的Git占用多大空间,这就需要进一步了解一下Git的原理了。
Git的简单讲解
Git的核心功能涉及到两块知识,第一个是Git的三个分区(工作区
、暂存区
、仓库区
),第二个是Git的快照存储三种形式(Blob
、Tree
、Commit
)。
对于Git的分区相信大家应该都了解一些,如果不清楚也不要着急,下面会说到。这里为了方便大家理解先说明一下Git的存储形式。
实践展示-存储单元
从这里开始,我会新建一个空文件夹来说明Git的相关存储形式。
git init
在初始化Git后,我们会看到文件夹根目录下创建.git
文件夹,而.git\objects
就是Git的存储目录
因为现在是个空文件夹,里面没有文件。
.git\objects
目录下除了Git生成的文件夹,没有其他内容
现在我们新建一个文件a.txt
,没有执行git add
的话,.git\objects
目录下依然没有内容
git add a.txt
执行后,.git\objects
目录中生成了94
文件夹和其目录下的ebaf900161394059478fd88aec30e59092a1d7
文件
代表着
a.txt
已经正式交给Git来管控。
而94
和ebaf900161394059478fd88aec30e59092a1d7
拼接后的94ebaf900161394059478fd88aec30e59092a1d7
为这个文件的hashId,这个文件就是a.txt
的快照。Git中称其为Blob
(后续会详细说明)。
ps. git cat-file是Git提供的查看快照文件的命令,了解即可,感兴趣可自行搜索。
这时我们提交一下试试
git commit -m "commit A"
.git\objects
目录中又新生成两个快照
我们通过
git cat-file
命令来查看一下
在Commit时,创建的
Tree
和Commit
的快照,并且构建了三个快照之间的关系(图中只写了hashId的前两位)
总结一下,Git在执行
git add
时创建相应文件的Blob
快照,在执行git commit
时,根据暂存区
的Blob
快照创建相应的Tree
和Commit
快照。完成存储。
实践展示-提交履历
上面介绍了Git的Commit的存储依赖关系,接下来我们再新建一些文件提交,增加一些文件结构和提交履历。
废话不多说,直接上图
提交后看一下
.git\objects
目录,增加了四个快照
为了节省篇幅我就直接上关系图了
根据关系图,我们可以通过
commit B
的hashId,获取到这个版本的所有文件。
到这里我们就可以了解到Git的存储机制了,如果不太清楚的话,建议停下来思考一下。
ps. 引申一点思考,这也解释了我们在提交代码时,只需要提交文件的原因,文件的目录是由Git通过创建Tree来创建的。
ps. 再记载一下一些小知识,如果两个文件的内容是一样,只是目录不一样。Git只会创建一个Blob对象。这可能是Git用来优化存储空间的一点小技巧。如果感兴趣可以自己试一下。
回头看一下Git的三个分区
读到这里大家应该对Git的存储形式有了大概的理解,我们回头来整理一下Git的文件分区
结合Git的存储形式可以得知,Git并没有实际文件分区,而是通过分布存储文件,实现了分区的概念。
ps. 再稍微扩展一下,在add之后创建的Blob对象,即使将文件移除暂存区,Blob对象仍然存在,如果有该文件的hashId的话,随时可以查看文件内容
Git指针(HEAD、branch、remote等,统称Reference)
如果理解了上面的内容,可能会发现一个问题。上面说Git都是通过hashId来存储、操作,但我们平时都是用branch
来操作的。聪明小伙伴已经猜到了,其实这些branch
、remote
只是一些指向某个Commit hashId的指针。
我们打开自己Git项目的.git\refs
目录来看一下,这里记录着指向的Commit hashId。
而HEAD
稍有不同,HEAD
记录的是当前所在的分支,可以看一下.git\HEAD
文件。
重新审视一下Git常用命令
现在,以我们掌握的知识,再来重新审视一下我们常用的Git命令,更清晰的认识Git。
git merge
我们知道,Git并不管理每个版本的变更,只管理每个版本的文件。而merge
则是将两个分支的文件进行合并,重新提交一个Commit,而这个Commit会有两个parent指向,这也就是我们平时看到的履历
git reset
其实reset
应该分开来讲,因为hard
、mixed
、soft
的区别,导致命令作用的差别。
但如果理解了其中本质的话,其他相信大家可以自己理解。
简单来说,reset
只是移动了当前branch
的commit指向。
比如执行git reset --mixed commit2hash
的话,本地文件并没有变更,仅仅变更了master分支的指向,如图所示
这里稍微的扩展一下,由于Git不会删除快照文件的特性,我们可以做一些奇怪但有效的操作。
再比如我们如果不小心merge
错分支的话,只要在log
中找到merge
前的commit hashId,执行git reset --hard commitHash
就可以恢复到之前的状态。
需要提醒一下,虽然Git不会删除快照文件,但是工作区的修改不在Git管理范围内,所以使用 reset --hard 时,需要慎重
git checkout
checkout
的功能同样很多,这里简单介绍一下常用的操作,其他功能大家可以自己思考一下。
执行git checkout branch1
时,首先会修改.git\HEAD
中的分支指向,然后抽出该分支对应的Commit下指向的文件,替换到我们的目录下。
工作区如果有修改和其快照文件有冲突时,无法抽出。
而执行git checkout file1
时,根据当前Commit中的Tree指向,找到该文件对应的Blob Hash,然后替换至本地目录下。
由于篇幅有限(懒),就写这么多吧。如果有兴趣的话,建议自己研究一下感兴趣的命令,会比其他人讲解收获更多。
总结
在学习Git原理时,我不只一次的感叹Git的设计,也希望大家在学习的过程中不仅仅只是会用就行。
如果了解了设计思想,不仅能提高我们使用的效率,还可以为我们打开思路。
举个简单的例子,空间换时间
的性能优化方针大家都懂,但像Git在遵从这种方针的同时,在一些细节上不断优化,这样才能配的上Git当前的地位。
转载自:https://juejin.cn/post/7203169721838927927