本篇文章适合有一定git使用经验的,看完之后再使用git会更加的得心应手;
没有使用过git的同学,可以先看下这个 git使用小手册(一)
先抛三个问题:
1、为什么说git是一个内容寻址的文件系统?
2、为什么git说分支是轻量的?
3、变基和merge有啥区别呢?
如果你对以上问题不清楚,就让我们带着问题,在本文中寻找答案吧!
一、.git文件目录
用git init初始化git仓库后,会生成一个.git文件夹。你可以把它当成是当前仓库的一个数据库,这里面几乎包含git对当前仓库进行管理时所需要的全部数据,例如有哪些分支、每个分支有多少版本,每个版本的仓库快照,远程仓库信息、钩子文件等。
1、hooks
hook翻译为钩子,顾名思义这个文件夹包含一些钩子文件,会在某些git命令被执行时触发。默认这个钩子文件是不可用的,具体使用方法在文件内的注释中有说明,比如改个文件名等。
2、info
exclude文件:作为.gitignore文件的扩展用。
refs文件:保存每个分支最新的一次提交的sha-1值。
3、logs
顾名思义存储git的操作日志,包括提交日志的sha-1值、拉取、合并、创建分支,分支切换、重置等的信息。但是根据git的设计原则,这里只是保存一些引用,相关数据以对象的形式保存到objects文件夹下。
logs/HEAD保存自己所有的git操作日志,
logs/refs/heads/ 保存本地仓库各分支的操作日志,
logs/refs/remotes/ 保存远程仓库的操作日志
4、objects
数据对象文件夹,git真正存储数据的地方,仓库各版本的数据都被压缩(zlib)和打包(packfile)后放到这里。用文件的形式保存数据,倒是符合Linux一切皆文件的哲学,嗯,有那味了~
这里主要分为三类文件夹:
1)以sha-1值的前两位命名的数据文件夹
sha-1值是个40为的字符串,git取前两位对文件夹进行分类,剩下的38位为数据文件名。
一般执行git commit后,会生成三个对象保存在这里,commit object、tree object、blob object,这些对象起初被认为是loose(松散,不紧的)对象。为了节省空间,git采用某种算法(类似JVM判断对象是否存活的可达性分析法)判断该对象是否为loose对象,然后采用zlib压缩算法对这些loose对象压缩,然后将结果分别保存到info和pack文件夹中。
2)info文件夹
类似打包文件的索引文件。
3)pack文件夹
压缩打包后的数据文件和索引。
5)refs
引用文件夹。为了找到每个分支最新的一次提交sha-1值,git采用专门的文件对齐进行存储。这里面包含本地仓库分支、远程仓库分支、标签、储藏等数据最新一次的sha-1,每次对应功能的sha-1发生改变时,git都将最新的数据写入到对应的文件夹内。
一些重要的文件:
config:
当前仓库的配置文件,包括远程仓库的信息、远程分支和本地分支的对应关系、当前提交者的信息等。具体的配置项可参考git config --help文档。值得一提的是,git有三种配置,OS级别的,当前级别的,当前仓库级别的,优先级是局部大于整体,这个和Linux下的环境变量的优先级也很像。
HEAD:
表示当前工作区处在哪个分支上。处于哪次提交由refs/heads/ 下对应分支的文件内保存的。
index:
表示当前暂存区中的文件树内容。该数据将会在git commit时跑到objects文件夹内。
COMMIT_EDITMSG:
表示当前分支最新一次提交的提交日志。
二、git特点和概念
3.1、git特点
1)分布式的VCS和本地仓库
在分布式VCS中,每个端都拥有完整的文件副本,能避免单点故障。git的本地库就是一套完整的VCS系统,意味着不需要联网就能进行版本控制。
2)出色的分支系统
a、十分方便创建和操作适合自己的开发分支模型(每个分支都是完全相互独立的工作空间)
b、基于分布式特点和分支系统,创建任何形式的git工作流程(见下文 10)git工作流)
3)先进的设计理念
- 拥有工作区、暂存区、本地仓库、远程仓库四种存储空间
- 采用引用和实际数据分开的方式保存版本数据。每次提交产生一个提交对象(commit object)和对应的版本快照,(就好比对象的引用和堆中实际存储的对象的关系)
4)速度快
比如分支切换时。
3.2 git核心概念
0)文件状态:
文件本身的状态:new file、modified、clean
git文件管理状态:untracked、tracked、unstaged(修改后),staged、clean
1)git仓库(repository):
仓库是git的一个术语,一个git仓库包括.git文件夹和被git所管理的当前工作目录。
2)工作区(work directory)
工作区就是当前用户所能看到文件目录,是某个分支某次提交检出后的物理文件。工作区的文件改动,将同步到暂存区,进而同步到本地仓库。
3)暂存区(stage)
暂存区表示对当前工作区工作进度的一次临时存储。暂存区只有一个版本,他和工作区一一绑定,当切换分支时,暂存区和工作区都会被重置。
git checkout -- fileName 能撤销工作区的改动,如果暂存区有新的暂存,git会用暂存区的文件重置工作区该文件,可以利用这点,对单个文件进行一次回滚操作,但这是回滚,不能穿梭。
4)本地仓库(local Repository)
5)远程仓库(remote Repository)
6)快照(snapshot):
相当于给当前项目拍个照片,做个样子定格一样。每次commit时,git保存的是当前项目的快照和引用。为了高效,对于改动的文件保存最新的文件快照,未改动的文件保存之前最后一次修改的引用。
快照文件内容保存在.git/objects 文件夹下,快照的引用保存在.git/logs 文件夹下。有了快照对象,有了对象的引用,很容易想到git也有对象的垃圾回收机制。
7)git对象
7.1)提交对象(commit object):
每次执行git commit后,git会在本地仓库上创建一个commit object和对应的的文件版本快照对象,commit中有个指针指向该版本快照对象。
这个commit object包含了父对象的对象校验和(可能会有多个例如merge时)、当前commit object的对象校验和(这个commit object很适合用链表来存储啊)、提交人信息和提交注释等信息。git在暂存和提交时,会对本次内容计算哈希值(利用SHA-1算法,40位字符串),这个哈希值称为对象校验和。
这个对象校验和有三个作用:
1、保证数据的完整性。
2、判断文件是否发生变动
(以上两点取决于哈希算法的特性:即相同的内容总是产生相同的哈希值)
3、当做唯一的指针指向某个版本快照对象。
多个commit object 之间构成一个链表,每个commit object拥有上一个提交对象的sha-1值,和对应的树对象的sha-1值。
7.2)树对象(tree object):
某次提交对应的快照文件树的索引,包含关联的blob对象的sha-1值。
采用 tree 这种数据结构来存储数据。
7.3)数据对象(blob object):
实际存储的文件快照,每个数据对象都有一个唯一的对象校验和(sha-1值)。
8)HEAD指针:
HEAD指针是个引用。为了让git使用者知道自己当前所处的位置,HEAD指针标识用户当前处在哪个的分支的哪个commit节点上。
两个文件保存了HEAD指针:.git/HEAD文件保存当前所处的分支;./git/refs/heads 保存了每个分支的最新提交的sha-1值。
9)分支(branch):
每个分支都是一个独立的工作空间,各分支之间互相不干扰。由于git先进的设计理念,使得git的分支变得十分快速和方便。传统的VCS创建分支是手动的将某个分支的物理文件复制一份,切换也需要手动切换文件夹,十分麻烦。git分支的创建只是生成一个包含开始节点对象校验和的文件,刚开始只有一个开始节点的对象校验和。分支切换也只是切到另一个分支文件中查询对象校验和,git会帮你根据对象校验和查询文件快照,从而更新当前暂存区和工作目录的文件。
在.git/logs/HEAD文件中保存当前项目的所有提交和切换日志,
.git/logs/refs/heads文件夹下,针对每个分支的提交日志都有个单独的文件保存着。
9-1)合并代码-》fast-forward(快进)
fast-forward是git执行分支合并的一种策略。例如从master分支的某次提交C1开启了hotfix分支,如果从C1点能直接向前走到hotfix最新的一次提交(和JVM的可达性分析法判断对象存活一样),此时hotfix分支和master分支合并时,git只是简单的将master分支的HEAD指针,向前移动到hotfix最新的一次提交,即表示合并完成。整个过程就好像电影快进一样,所以称之为fast-forward。
9-2)合并代码-》三方合并
三方合并(Recursive,递归)是git执行分支合并的另一种策略。三方合并就是三方的合并,会有三个分支。假设dev1和dev2都开辟自master的C1提交,dev1分支提交了几次代码,dev2分支也提交了几次代码,此时想将dev1和dev2合并,git采取的策略是,用master的C1提交和dev1、dev2中的最新提交来个简单的“三方”合并。
与fast-forward不同的是,他会自动产生一个新的提交快照,该提交称为合并提交。
如果在合并时产生冲突,git会自动合并保留这些冲突,等待用户手动解决冲突后,手动提交。
使用工具的好处是工具能提供相对方便的方式来解决这个问题。
9-3)变基VS merge
首先,rebase和merge对于快照合并结果是一样的,不同的是对于提交日志。变基使得合并后的提交日志是一条线,而合并有明显的产生分支和合并分支的交叉点。他们本质上是对.git/logs中提交日志文件内容的修改。变基是将当前分支的整个提交日志,整体移动到目标分支的最新提交上,可能本来是从master的C1产生开始提交日志,变基后开始提交日志就变成目标分支的最新一次提交了。
如果为了使整个项目的提交日志看起来更整齐,可以使用变基操作:
git checkout -b feature #假设在dev分支上开辟一个feature分支
some commit #执行多次提交,功能开发完毕
git rebase dev #变基到dev。注意此时仍处于feature分支,属于feature在上dev在下的并行提交,要完全合并成一个,还需要merge
git checkout dev #切换到dev分支
git merge feature #dev合并feature的代码,属于fast-forward合并
git branch -d feature #删除该特性分支
之后有新功能时,再次执行上述操作,这样dev的提交日志就是一条线了。如果使用git merge就两条线,创建多个分支就是多条线。
10)git工作流
基于Git 的分布式特性和出色的分支系统,可以相对轻松地实现几乎无穷无尽的工作流。
常见的三种:
1)集中式工作流程(svn式,传统式)
2)集成管理器式工作流程(pr式)
3)独裁者和副官式工作流程(适合大型分布式微服务系统,多模块多子系统时)
五、git如何存储和查找数据的?
要理解git是如何存储和查找数据的,这里有几个词很关键——三棵树、四个区域、.git文件夹,git实质上是在这几个区域内来回捣腾数据。
ps:三棵树,指的是当前工作区的树、暂存区的树、本地库的某个提交的树。其中三棵树和四个区域算是逻辑上的结构,.git文件夹才是物理上的结构。
这里以一个基本的操作流程为例:
1、git clone
克隆会直接将远程仓库的.git文件夹内容直接拉取过来,初始化本地仓库,同时创建一个同名的本地分支track(追踪)远程分支,默认是master分支。然后将该分支的最新提交快照检出到当前工作区,同时重置暂存区。 更新config等相关文件。
2、git add
之后你会新增文件(untracked)或修改原有文件(modified),此时你的工作区的树和暂存区的树就不一样了,你可以使用git diff [option] 命令查看差异内容。此时如果你执行git add xxx命令会将差异同步到暂存区,使用git status命令,git会告诉你有哪些文件需要提交。此时你可以选择提交或者继续修改。
当你执行git add时,意味着暂存区的树内容发生了变动,git会用sha-1算法计算新的哈希值。
值得一提的是,暂存区相当于一次"小的commit",当你对工作区的想法不满意想要回到暂存之前时,可以执行git checkout -- file 等命令,来进行一次所谓的“回滚”。但是注意,这里的回滚不同于版本穿梭,“你滚回去就不能再滚回来啦”,也就意味这你的内容将永久丢失。注意注意!
3、git commit
git commit所做的其实是git存储数据的过程
提交后,git会生成三个对象——提交对象、树对象、blob对象。这三个对象都保存在.git/objects的某个文件内,为了区分不同的对象类型,git在计算对象的sha-1值时,会先构造一些头部信息,例如 "blob #{content.length}\0", 这个头部信息分三块,第一块为对象的类型,分别为commit、tree、blob,然后将头部信息拼接上实际的数据,从而计算得出“对象校验和”。在文件内容方面,git会根据./git/HEAD文件中的当前分支,找到并更新./git/refs/heads/下对应分支的sha-1值,同时在./git/logs下增加一条操作日志。
这个提交对象就是个提交日志,其中最重要的信息时父对象的sha-1值和关联树对象的sha-1值,还有少许的提交信息,并不存储真实的文件树快照,每个快照中文件的数据还要交给了树对象来完成。
这个树对象在git commit时,保存暂存区文件树的快照。树上面有很多树entry,每个entry都是一个引用对象,一个entry基本对应文件树中的一个文件,其中一个很重要的属性是代表文件内容的blob对象的sha-1值。为了高效,对于没有改动的文件,git只是保存了原文件的引用,对于改动的文件,git也会计算两个版本之间的差异,将差异保存在上一版本,最新版本总是最新最完整的文件数据。
最后,git会将经过差异比较后的文件内容,保存为一个blob对象,存放到./git/objects 文件夹内。
为了进一步节省空间,git会定时对objects文件夹中的文件进行打包,生成一个packfile,这个在上面讲一、.git工作目录时说过。
所以,经过上面的分析,我们发现,git以链表的形式保存每个分支上的提交对象,一个链接的节点代表一个提交对象,一个提交对象唯一关联一个树对象,树对象以一对一或一对多的形式关联这blob对象。这里没有准备图片,大家可以根据理解自己画一画三个对象之间的关系!!!
4、git branch
如果用git branch 创建一个分支,实际上只是创建了两个文件
1、./git/logs/refs/heads/分支名 #准备记录操作日志
2、./git/refs/heads/分支名 #记录分支最新提交的sha-1值
5、git checkout
如果用git checkout 分支名来切换分支时,git做了哪些事呢?这其实也是git查找数据的过程,大致如下:
首先git 会到/.git/logs/refs/heads/分支名 文件中找到最新的一次提交的sha-1,然后git会根据这个提交对象的sha-1值,到objects下找对应的数据对象,取出里面的tree对象的sha-1值,再根据tree对象的sha-1值找到objects中对应的tree对象,根据tree对象和关联的所有的blob对象,直接重置暂存区和当前工作区。
此外git还会更新HEAD文件中的当前分支名和./git/refs/heads/分支名文件中的当前提交的sha-1值等信息。
注意,这个时候你未提交的数据将会丢失,但是git早就知道这点,如果使用命令行时git会阻止你切换分支,关于此时如何切换,可以查看 git使用小手册(一) 中的切换分支操作。
6、git reset
如果使用git reset进行版本穿梭的话,git会将head指针在当前分支的链表上移来移去,找到那次提交的sha-1值,然后按照上面git查找数据的过程到仓库中查找数据,git reset不是有三个参数吗,git会根据不同的参数,选择重置不同的区域树。然后再更新最新提交的sha-1和index等文件信息。
好了,别的命令就不再说明了,其他命令基本上也都大同小异。
怎么样,现在是不是对于开头说的“git实质上是在这几个区域内来回捣腾数据”,以及上一篇文章git使用小手册(一) 中说的 “git本质上是一个内容寻址的文件系统”,有了更深的理解了呢?