1.名词介绍

image.png
commit 就是一个当前代码及资源的快照,它在底层是以一个提交对象的形式存在。这个提交对象不止包含快照,还包含它指向的内容的指针、作者的邮箱及名字、commit 提交时输入的更新信息、父提交对象的Hash值(首 commit 没有)。
diff: 两个 commit 之间的文件差别是diff
branch: 由于 branch 的存在,多个commit 的父对象可能指针相同
merge: 一个由合并而产生的 commit 有多个父对象指针。

2. 底层原理

0.Git 存储文件底层原理:

计算出当前未提交的文件内容的Hash值、及将内容转成二进制的形式存储到文件当中。这个文件就放在.git/objects 目录下。由于不同的文件系统,对目录下文件的数量有限制,且考虑提高目录检索的效率,每个文件都会单独建立一个文件夹,文件夹的名字是Hash值的前2位,文件的名字是Hash值的后38位。

1. git add 原理

当执行git add .命令时,底层会依次执行下面 3 个命令:

  • git hash-object 计算文件生成 Hash 值
  • git update-index 将需要放到暂存区的文件的基础信息, 更新到暂存/索引区即 .git/index 文件中
  • git write-tree 把当前索引区存储的文件信息以二进制的形式存储到一个新的文件中,也在objects文件夹下,也是以前两位为文件夹的名称,后面为文件的名称

Git 工作原理 - 图2

1. git hash-object

计算指定文件,生成 Hash 值

  1. // 计算 Hash 值
  2. git hash-object <file path>
  3. // 计算 Hash 值,并写入.git/objects文件夹
  4. git hash-object -w <file path>
  • 将每一个修改的文件计算出一个40位 hash 值作为 key
    • 如果文件的内容改变了,再次进行 hash 计算时,会产生一个新的 hash 值。
    • 如果文件内容再变回去,再次计算时,发现已经存在一个相同的 hash 值,便不会再重新创建新的文件
  • 将每一个修改的文件的内容以二进制的形式存储到一个文件中,作为 value
  • .git/objects 会新建一个文件夹和一个文件
    • 文件夹的名字是 key 的前 2 位;文件的名字是 key 的除去前 2 位的剩余38位.即:
      • .git/objects/hash[:2]/hash[2:40]
    • 这么做的原因是:
      • 部分文件系统对文件夹下的文件的数量有最多限制. 例如FAT32限制目录里最大文件的数量是65535个
      • 部分文件系统查找文件是线性查找,目录下的文件越多,访问越慢

Git 工作原理 - 图3

2. git update-index

将需要放到暂存区的文件信息, 更新到暂存/索引区即 .git/index 文件中

// 将文件的mode/Hash值/文件路径,更新到索引区
git update-index --add --cacheinfo <mode>,<sha1>,<file path>

// eg
git update-index --add --cacheinfo 100644 335311d6c2ad7e8e00664fbe1be148b31b916117 test.m

// 查看暂存区index 文件的内容
git ls-files -s

Git 工作原理 - 图4

2.1 mode

表示文件的类型,是一个八进制的数值。在 Git 里,几个常用的 mode 值包括:
1. 100644 - 普通文件;
2. 100755 - 可执行文件;
3. 120000 - 符号链接(symbolic link);
4. 040000 - 目录;

100644的二进制是110 100 100, 每三位一组,表示三组不同用户。每组里面的三位含义分别为可读、可写、可执行。
Git 工作原理 - 图5
Git 工作原理 - 图6
100644表示:

  • 最高权限的用户:可读、可写、不可执行
  • 组用户:可读、不可写、不可执行
  • 其他用户:可读、不可写、不可执行

    3.git write-tree

    把当前索引区存储的文件信息生成一个目录树的节点,即以二进制的形式存储到一个新的文件中,也在objects文件夹下,也是以前两位为文件夹的名称,后面为文件的名称。 ``` // 把暂存区的内容生成一颗树的节点,并且打印出来这个节点的hash值 git write-tree

// 查看文件内容,-p 更优美的打印hash值代表的文件内容 git cat-file -p 335311d6c2ad7e8e00664fbe1be148b31b916117

下面是**暂存文件**的实例,读取新的树节点的内容,可以看到是之前暂存的2个文件:<br />![](https://cdn.nlark.com/yuque/0/2021/png/94660/1618656385451-d3e45594-e02d-4ec4-b000-9f486604c008.png#clientId=u298a0f2a-21ee-4&from=paste&height=244&id=ub7d45783&margin=%5Bobject%20Object%5D&originHeight=488&originWidth=2298&originalType=binary&size=347312&status=done&style=none&taskId=u6325c3e3-ed05-4e8a-bd6d-051119bff09&width=1149)<br />下面是**暂存文件夹**的实例,读取新的树节点的内容,可以看到是之前暂存的2个文件和新的一个文件夹:<br />![](https://cdn.nlark.com/yuque/0/2021/png/94660/1618660790538-7d0e3498-dac9-4d46-ae35-02a3c30f1616.png#clientId=u298a0f2a-21ee-4&from=paste&height=310&id=uca4b88d8&margin=%5Bobject%20Object%5D&originHeight=620&originWidth=2050&originalType=binary&size=422933&status=done&style=none&taskId=uf2129a1b-3f50-4ba8-90cd-7e7565ca102&width=1025)
<a name="SZPrS"></a>
### 4.git 中的文件夹原理
<a name="EuJ11"></a>
#### 4.1 git 中没有存储文件夹,只有文件
在上面查看暂存区信息的时候,我们发现只是增加了一个文件"test/test1.m" ,而非增加2个:一个“test”文件夹,一个“test1.m”。这说明,**在 git 中是不单独存储文件夹的,在暂存区存储的只是文件的名字**(如果有目录的话,带着目录结构)**。**文件夹是通过**文件的目录路径**及树节点中**类型为040000的节点**确定的。

我们可以在做一个实验来证明一下:单独建立一个文件夹,然后`git status`可以看到并没有提示出来有需要存放暂存区的东西。这就是因为在**git 中没有存储文件夹,只有文件**。<br />![](https://cdn.nlark.com/yuque/0/2021/png/94660/1618831139775-1b37a9b7-d9d0-4f55-9a54-b3a81ce4f814.png#clientId=u298a0f2a-21ee-4&from=paste&height=219&id=uba00358f&margin=%5Bobject%20Object%5D&originHeight=438&originWidth=1388&originalType=binary&size=194917&status=done&style=none&taskId=u4d66d7b4-065e-4811-9e23-3957cc7b4bc&width=694)
<a name="RmrSG"></a>
#### 4.2 如何提交空文件夹
如果想提交一个空的文件夹,是会提示没有需要提交的东西的,因为在 **git 存储底层是没有文件夹的,只有文件**,我们能看到的文件夹,都是由文件的路径构建出来的。如果想要提交一个空的文件夹,可以在**空的文件夹中创建一个以.keep/.gitkeep结尾的文件**,这样就可以提交了。<br />像下面这样:<br />![](https://cdn.nlark.com/yuque/0/2021/png/94660/1618831507063-9a39fad1-5ec3-4122-bf51-fab93cc44c96.png#clientId=u298a0f2a-21ee-4&from=paste&height=274&id=u731ca22f&margin=%5Bobject%20Object%5D&originHeight=548&originWidth=1894&originalType=binary&size=229419&status=done&style=none&taskId=ucb07eced-fb3c-443d-a4f3-bfb90150612&width=947)
<a name="PqY84"></a>
#### 4.3 手动构建一个文件夹结构
我们创建一个文件夹,里面再放入几个文件。这样的动作可以通过使用如下的 git 命令直接实现:

// 1.为某个节点创建一个父节点,及父目录 git read-tree —prefix=Animal a21c9362e8eece5e48de1c9f02721eb234d128d3 // 2.将新的目录写入目录树,并创建节点 git write-tree

![](https://cdn.nlark.com/yuque/0/2021/png/94660/1618832737695-9ea26189-e960-4763-9c84-c5d80cd3daa0.png#clientId=u298a0f2a-21ee-4&from=paste&height=519&id=ub648c726&margin=%5Bobject%20Object%5D&originHeight=1038&originWidth=1676&originalType=binary&size=655326&status=done&style=none&taskId=u9c313b14-7b9a-4cf9-a132-17f4ff3f5a4&width=838)<br />这样操作就实现了额外的**创建了一个Animal 的文件夹,并且里面放入了当前项目根目录下的所有的文件**。

<a name="c7Hbr"></a>
## 2. git commit 原理
创建一个提交对象:

1. 将`git add `过程中,`git write-tree `生成的目录树的节点,使用命令`git commit-tree`生成提交对象
1. 提交对象创建完,需要**手动清空暂存区**、并且将**当前HEAD指向新的提交对象**

// 提交暂存区生成的目录树的节点,会生成一个提交对象 echo ‘init git’ | git commit-tree d34a421c9fab04e66692f841991b0f440fc2b882 // 提交暂存区生成的目录树的节点,会生成一个提交对象,并且指定他的父节点 echo ‘third commit’ | git commit-tree d34a421c9fab04e66692f841991b0f440fc2b882 -p 60a136511608b45b6e1817cf052e80474afdb8af

// 查看提交对象 git log —oneline —decorate —graph —stat d34a421c9fab04e66692f841991b0f440fc2b882

![](https://cdn.nlark.com/yuque/0/2021/png/94660/1618835110433-7c8d1477-3dc3-441e-81b6-db0010fff5a6.png#clientId=u298a0f2a-21ee-4&from=paste&height=114&id=uc43a08da&margin=%5Bobject%20Object%5D&originHeight=228&originWidth=1708&originalType=binary&size=156187&status=done&style=none&taskId=u19d01893-ad2b-4795-9a65-cf7db790077&width=854)<br />再次提交新的commit 的时候,可以**指定**新的提交对象的**父节点**是刚才创建的60a1365xxx:<br />![](https://cdn.nlark.com/yuque/0/2021/png/94660/1618835251028-1f500ae7-bb34-4c74-864a-06ce9df03aa7.png#clientId=u298a0f2a-21ee-4&from=paste&height=28&id=u073cc7e9&margin=%5Bobject%20Object%5D&originHeight=56&originWidth=2366&originalType=binary&size=43988&status=done&style=none&taskId=u1162cbae-d8c7-48bb-8616-e0f0f2641f4&width=1183)<br />![](https://cdn.nlark.com/yuque/0/2021/png/94660/1618835372883-02e99bb5-7ab8-4d34-92fa-6e08fb4a9c9c.png#clientId=u298a0f2a-21ee-4&from=paste&height=102&id=u6c278aaf&margin=%5Bobject%20Object%5D&originHeight=204&originWidth=1624&originalType=binary&size=78258&status=done&style=none&taskId=u609dafd5-7d5d-4ee7-aeb8-deecaa26817&width=812)<br />这时,我们手动构建的 commit 就完成了。但是呢,还2个问题:

- 暂存区内的文件还依然存在
- git log 并不能看到我们的commit

正常我们 `git commit `的时候,git 会帮助我们**自动清空暂存区**,并且**把head 指向咱们的最新提交对象**。<br />现在我们是自己手动创建commit 对象,所以需要手动完成这两个操作。用下面2个命令可以做到:

// —-cached清空缓存区,-r 递归清空 git rm —-cached -r .

// 把head 指向指定提交对象 git reset —-hard commitId

<a name="qeg9C"></a>
## 3.总结
在在我们执行 `git add `和 `git commit `的2个命令时,底层是帮我们做了下面6个动作:

// 1. 计算并生成hash git hash-object -w test.m // 2. 把hash值更新索引区。所以索引区只保存hash值,不保存改变文件的元数据 git update-index —add —cacheinfo 10064 539d2878f3ca258c95d459aa19fe28e865c5e2c7 test.m // 3.生成目录树的一个新的节点 git write-tree
// 4.根据新节点创建出一个提交对象 echo ‘retry’ | git commit-tree ba03c472f28d59e2ae028143b425cd6e59a74d8b -p 60a136511608b45b6e1817cf052e80474afdb8af // 5.移除暂存区的内容 git rm —cached -r . // 6. 将当前 HEAD 指向新的提交对象 git reset —hard 4b5008b195fcf0920370d999fd00c7a71d8cf201 ``` Git 工作原理 - 图7