Git是一个开源的分布式版本控制系统,目前大多数团队都采用Git作为团队内部的版本控制系统,并且在DevOps的整体流程中,版本控制是其中重要的一环,在“基础设施即代码”的理念下,不止是系统源代码,系统配置纳入版本管理,数据和环境配置也要纳入版本管理,可以说,版本管理使得研发流程可复用、标准化、自动化,进而提高了研发效率。
大多数人在使用git时,大多数都是使用IDE封装好的GUI界面操作,少量使用者也是git add,git commit,git pull几个常用的命令。对于git的用户来说,这些都足够了。如果你是一个DevOps开发者或是想成为Git的深度用户,知道这些是远远不够的。下面就让我们了解一下Git背后的逻辑。
第一、Git的三个区+一个远程仓库

这个图好多人都应该见过。里面也是一些经常使用的命令。主要包含几个部分:
①Remote:远程仓库,像github就是一个远程仓库。
②Repository:本地仓库,通过git clone将远程仓库的代码下载到本地。代码库的元数据信息在根目录下的.git目录下。
③Workspace:工作空间,就是我们写代码的目录。
④Index:暂存区,指的是.git目录下的index文件。
在平时写完代码后执行git add 就是将变更的内容从工作空间提交到暂存区,git commit就是将暂存区的内容提交到本地代码库里,git push 就是将本地代码库的变更提交到远程仓库,这时其他人就能通过pull 将你的变更下载到工作空间。
第二、git的内部存储结构
新建一个gittest目录,然后执行git init就会初始化一个本地仓库。打开.git目录文件列表如下:

hooks:是存储git钩子的目录,钩子是在特定事件发生时触发的脚本。比如:提交之前,提交之后。
info:是存储git信息的目录,比如排除特定后缀的文件.
objects:是存储git各种对象及内容的对象库,包含正常的和压缩后的。
refs:是存储git各种引用的目录,包含分支、远程分支和标签。
config:是代码库级别的配置文件。
HEAD:是代码库当前指向的分支,这里为master。
一、新建README.md文件
新建README.md文件,并输入内容“This is test file!”

git add README.md 将工作空间的变更提交到暂存区。
.git/index文件会被修改,通过git ls-files --stage 查看暂存区的内容,可以看到README.md文件有修改,通过git cat-file -p SHA-1查看文件内容。

同时.git/objects下也会有新的对象,如下:

这个对象就是刚才add到暂存区的对象,在.git/objects目录下看到一个文件。这便是Git存储数据内容的方式--为每份内容生成一个文件,取该内容与头信息的SHA-1校验和,创建以该校验和前两个字符为名称得子目录,并以校验和剩下38个字符为文件命名。这里并没有显示真实的文件名。
通过git cat-file -t SHA-1查看对象的类型。blob代表文件。

二、创建一个目录web,并添加一个web.txt文件

执行git add web/ 添加到暂存区。
查看暂存区内容,变成了2条记录,存储方式和上面文件一样。

可以看出,这里只有文件内容生成的文件,没有为目录生成文件。
在.git/objects下面也创建了相应的目录和文件。

三、提交内容
到目前为止,暂存区的内容有README.md和web/web.txt文件。下面通过git commit 将暂存区的内容提交到本地仓库。

使用git log 查看本次提交信息,使用git cat-file -p查看变更内容,当类型为tree时表示文件夹,会显示该文件夹下的文件或目录列表,当类型为blob时为文件,会显示该文件的内容。

因此本次commit的存储模型为:

此时再查看.git/objects目录下,有增加了几个目录。其中:
8d:提交的commit对象
58和ff:提交的commit对应的tree对象和web目录的tree对象。
a1和b3:提交的README.md文件和web/web.txt文件对应的数据对象。

可以看出,当我们执行git add 和git commit 命令时,Git做的工作是将被改写的文件保存为数据对象、更新暂存区,记录树对象,最后创建一个指明了顶层树对象和父提交的提交对象。这三种Git对象(数据对象-blob、树对象-tree、提交对象-commit)最后均以单独文件的形式保存在.git/objects目录下。
四、新建分支test
上面提到分支、远程分支和标签都会存储在.git/refs下。

heads包含分支,tags包含标签。每个引用文件里都会指向一个commit,如基于master分支新建的test分支 git branch test:

使用git checkout test切换到test分支,此时HEAD的内容为refs/heads/test,表示当前分支为test.

在test分支下修改文件内容:

git status查看工作空间状态,有两个修改过的文件。

git add .将变更的文件添加到暂存区,查看暂存区内容发现:
README.md的校验和从之前的a177f89--->7a51843
web/web.txt的校验和从之前的b31494b--->f5ebb2c
查看文件内容也是最新修改的内容。

.git/objects目录下也多了2个新的目录

git commit 将暂存区的内容提交到本地仓库。和上面一样,此时也会创建提交对象,树对象,通过查看不同的对象,最后都能查看到具体的数据对象的最新内容。

同时在.git/objects下也会创建提交对象c7,树对象cf和树对象a3

五、分支合并冲突
在master分支修改web/web.txt,将内容改为“this is a old web.txt”,以便产生冲突。
git add 添加到暂存区,查看文件内为新修改的内容。

git commit添加到本地仓库,同时生成提交对象,树对象。

执行git merge test进行分支合并,web/web.txt出现冲突。

分支合并后,master分支的commit由之前的 8da82ba变为c75be2f。
暂存区中的README.md的校验和也由master分支的 a177f89变成7a51843。
而web/web.txt变成了3个文件,b3是master当前commit的父commit的校验和,18是master最新提交的校验和,f5是test分支上的校验和。

解决冲突,两行内容都保留,结果如下:

git add 添加到暂存区,查看暂存区内容,README.md是test分支上的,web/web.txt是解决完冲突新生成的校验和。

也就是说,分支合并会修改当前分支的commit信息以及暂存区的内容,当解决完冲突后,将冲突文件git add 添加到暂存区,然后git commit 将合并后的内容提交到本地仓库。


也就是说,合并完冲突后,只有将更新后的内容commit,在生成的提交对象上才能找到新加的内容。
这里回答下遇到的一个问题:
问题:我当前分支是test分支,当从master分支合并过来后冲突了,我解决完冲突,只提交了这个文件,其他文件不用提,因为其他文件的内容已经在master分支上了,当我从test分支再合并回master分支后,也应该没有问题。
解释:根据上面的分析,每个commit都是一个提交对象,这个提交对象关联一个树对象,树对象会包含最新的数据对象。当从master分支合并后,master的commit提交对象包含的树对象和数据对象,会作为新的文件内容存放到test分支的暂存区,此时有2种选择:
①,当在test分支commit时,这次生成的提交对象才会包含master过来的文件内容。
②,如果此时放弃提交(手动checkout当前分支最新的),那么当前commit提交对象不包含master的文件内容,当test分支合并回master时,也会将test分支的文件内容存放到master分支的暂存区,当push到远程仓库时,也就是test分支的数据对象的内容,因此其他人拿到的文件内容就会丢失。