从git init 执行后发生了什么我们知道,Git 经过初始化后,会形成三个主要区域:工作目录(Working Directory)、暂存区(Staging Area)和版本库(Repository):
使用 Git 的工作流程是:先将文件放入工作目录中,然后使用 git add <文件名>
命令将该文件添加到暂存区,接着使用 git commit
提交到版本库中 。
下面就具体看看暂存区的机制和原理吧
Git的暂存区(Staging Area)是位于Git仓库内部的一个中间区域,也就是版本库 .git
目录下的 index
文件。暂存区的含义是,在对项目文件进行修改后,这些修改并不会立即被提交到版本库中。相反,你需要将这些修改先添加到暂存区,然后才能将其作为一个整体提交到版本库中。
要将修改的文件添加到Git的暂存区中,可以使用 git add <文件名>
命令,我们可以利用 git ls-file
查看index
文件中的内容,判断文件是否添加到暂存区中。
//创建一个testIndex.txt文件
$ echo "test index" > testIndex.txt
//先查看暂存区中的文件,目前有一个文件和文件夹,没有testIndex.txt文件
$ git ls-files --stage
100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 lib/readme2.txt
100644 b0530c9b7360a8cea0e4af86475cac70a2985138 0 readme.txt
//使用git add命令后,再次查看暂存区,发现
$ git add testIndex.txt
$ git ls-files --stage
100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 lib/readme2.txt
100644 b0530c9b7360a8cea0e4af86475cac70a2985138 0 readme.txt
100644 b86453316b1e4fb7bd6974d9dc0ff029a4e60f48 0 testIndex.txt
这个命令将指定的文件添加到暂存区中,准备将其包含在下一次提交中。也可以使用通配符来匹配多个文件。比如想添加所有修改的文件到暂存区,可以使用
git add .
命令,这将添加当前目录下的所有修改的文件到暂存区中。
上面输出的内容我们在下面讲解 Index 文件内部内容后再讲解
另外,如果你想要交互式地选择要添加到暂存区的文件,可以使用以下命令 git add -i
,此时进入交互式的模式,让你逐个选择要添加的文件。你可以根据提示进行操作,选择要添加的文件并确认操作:
.git/index
文件实际上就是一个包含文件索引的目录树,记录文件名和文件的状态信息,文件具体的内容存储在 Git 对象库,也就是 .git/objects
目录中,如下图所示:
git add <文件名>
:将指定文件从工作区添加到暂存区。暂存区的目录树将会被更新,文件内容会被写入到对象库中的一个新对象中,该对象的 ID 将会被记录在暂存区也就是 Index 文件中。git restore --staged <文件名>
:将指定文件或修改从暂存区移除,但保留在工作区中。这个命令会撤销之前使用 git add
命令添加到暂存区的文件或修改。git checkout -- <文件名>
:撤销对指定文件的修改,将其恢复到最近一次提交的状态。这个命令会影响工作区中的文件,但不会改变暂存区。git checkout HEAD
: 会用 HEAD 指向的 master 分支中的全部文件替换暂存区和工作区的文件git reset HEAD <文件名>
:将指定文件从暂存区移除,但保留在工作区中。这个命令可以用于撤销之前使用git add命令添加到暂存区的文件。git commit
:将暂存区的内容作为一个整体提交到版本库中。这个命令会将暂存区中的文件和修改保存到版本库中,不会直接影响工作区和暂存区。master 最新指向的目录树就是提交时原暂存区的目录树。git rm --cached <文件名>
: 直接将暂存区的该文件删除,工作区则不做出改变Index 文件是一个二进制文件,位于 Git 仓库的 .git 目录下,具体路径是 .git/index
。它是一个索引文件,里面包含了一系列的记录条目。每个记录条目对应一个被跟踪的文件,记录了文件的元数据和状态信息。
此外 Index 建立工作目录中的文件和对象库中对象实体之间的对应关系。其与工作目录,版本库的关系可以继续引用上一节的图:
前面提到过可以使用 git ls-files
命令来查看 index 文件,它主要有两个参数:
-c
,-cached
:默认选项,只显示已经暂存的文件名-s
,--stage
:除了暂存的文件名,还有模式,暂存区编号等等信息比如上一节中的 index 文件输入如下:
$ git ls-files --stage
100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 lib/readme2.txt
100644 b0530c9b7360a8cea0e4af86475cac70a2985138 0 readme.txt
100644 b86453316b1e4fb7bd6974d9dc0ff029a4e60f48 0 testIndex.txt
040000
:目录100644
:普通文件。用于存储文本、二进制数据或程序代码等100664
:普通文件,和 100644 的区别在于它具有默认的文件权限,即所有者具有读取和写入权限,而组和其他用户只有读取权限。100755
:可执行文件,具有可执行权限的文件,通常是二进制可执行程序或脚本文件120000
:符号链接,指向另一个文件或目录的特殊文件类型160000
:Git 子模块是引用其他 Git 仓库的特殊文件类型。它允许将一个 Git 仓库作为另一个仓库的子目录进行管理我们可以用 xxd
命令(默认显示 16 进制,加上 -b
参数为二进制模式)来查看上面存储文件的 index 文件内容:
$ xxd .git/index
00000000: 4449 5243 0000 0002 0000 0004 65a7 3bef DIRC........e.;.
00000010: 2de3 5dc8 65aa 2362 3187 05b0 0000 0000 -.].e.#b1.......
00000020: 0000 0000 0000 81a4 0000 0000 0000 0000 ................
00000030: 0000 0006 4632 e068 d588 9f04 2fe2 d925 ....F2.h..../..%
00000040: 4a92 95e5 f31a 26c7 000f 6c69 622f 7265 J.....&...lib/re
00000050: 6164 6d65 322e 7478 7400 0000 65a6 3ff2 adme2.txt...e.?.
00000060: 265c fdd4 65a6 3ff2 266c 377c 0000 0000 &\..e.?.&l7|....
00000070: 0000 0000 0000 81a4 0000 0000 0000 0000 ................
00000080: 0000 0017 b053 0c9b 7360 a8ce a0e4 af86 .....S..s`......
00000090: 475c ac70 a298 5138 000a 7265 6164 6d65 G\.p..Q8..readme
000000a0: 2e74 7874 0000 0000 0000 0000 65a7 9118 .txt........e...
000000b0: 0a83 0978 65a7 9118 0aa1 77b4 0000 0000 ...xe.....w.....
000000c0: 0000 0000 0000 81a4 0000 0000 0000 0000 ................
000000d0: 0000 0795 4a8e 6de9 6c10 f56a f756 2a3e ....J.m.l..j.V*>
000000e0: 7142 a0e7 386e 9f23 0009 7365 636f 6e64 qB..8n.#..second
000000f0: 5461 6700 65a8 c995 1868 fca0 65a8 c995 Tag.e....h..e...
00000100: 1875 3650 0000 0000 0000 0000 0000 81a4 .u6P............
00000110: 0000 0000 0000 0000 0000 000b b864 5331 .............dS1
00000120: 6b1e 4fb7 bd69 74d9 dc0f f029 a4e6 0f48 k.O..it....)...H
00000130: 000d 7465 7374 496e 6465 782e 7478 7400 ..testIndex.txt.
00000140: 0000 0000 5452 4545 0000 000f 002d 3120 ....TREE.....-1
00000150: 310a 6c69 6200 2d31 2030 0a9d 14da 2654 1.lib.-1 0....&T
00000160: 6c71 7806 f73b f4d8 0256 9c92 430e 1c lqx..;...V..C..
Index 文件由以下内容组成:
Index的开头部分包含一个固定的头部,其中包含了签名,版本号、索引记录的总条目数:
从我们上面输出的 index 文件内容,取前 12 字节,可以知道其 Header 的内容为:
00000000: 4449 5243 0000 0002 0000 0003
4449 5243
对应的是DIRC。0000 0002
对应内容是2。0000 0003
对应条目 3 条我们再来看看实际 index 中的文件数:
$ git ls-files --stage
100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 lib/readme2.txt
100644 b0530c9b7360a8cea0e4af86475cac70a2985138 0 readme.txt
100644 b86453316b1e4fb7bd6974d9dc0ff029a4e60f48 0 testIndex.txt
此时文件条目数为 3
文件索引记录(Index Entry)是Git索引(Index)中的关键部分,用于表示 Index 中的每个文件。每个文件索引记录包含了文件的修改时间,文件模式,文件名,对象 ID 等等信息,下面仍然以实验中用的 index 文件来讲述其不同部分的组成:
先来看看对应上面二进制 Index 文件中对应的数据,第一个 entry 是 lib/readme2.txt
文件
65a7 3bef 2de3 5dc8 65aa 2362 3187 05b0
可以利用 stat
来查看lib/readme2.txt
文件的更改时间和文件内容的修改时间:
//1.文件系统创建时间
$ stat lib/readme2.txt
...
Access: 2024-01-19 17:09:29.301397900 +0800
Modify: 2024-01-19 15:23:14.830932400 +0800
Change: 2024-01-19 15:23:14.830932400 +0800
Birth: 2024-01-17 10:31:11.769875400 +0800
$ date -d "2024-01-17 10:31:11.769875400 +0800" +%s
1705458671
$ printf '%x %x' 1705458671 769875400
65a73bef 2de35dc8
//2.文件系统修改时间
$ stat -c 'btime: %Y %y' lib/readme2.txt
btime: 1705648994 2024-01-19 15:23:14.830932400 +0800
$ printf '%x %x' 1705648994 830932400
65aa2362 318705b0
发现文件的系统创建时间和修改时间和 index 文件中编码值一一对应。
dev 设备信息,共 4 字节,表示文件对应的设备信息,在本 index 文件下的值:
0000 0000
inode 编号信息,共 4 字节,表示 index node,索引节点
0000 0000
也就是文件类型和权限,当前文件下是普通文件:100644,对应的也就是
0000 81a4
前面的 16 位为 0,查看系统中的文件,与 index 中编码相符
$ stat -c '%f' lib/readme2.txt
81a4
0000 0000
0000 0000
index 文件中的编码,显示的文件大小
0000 0006
可以用 stat
来查看系统中文件大小:
$ stat -c '%s' lib/readme2.txt
6
index 文件中显示的 ID 值:
4632 e068 d588 9f04 2fe2 d925 4a92 95e5 f31a 26c7
可以使用 git hash-object
来查看文件的 ID 值:
$ git hash-object lib/readme2.txt
4632e068d5889f042fe2d9254a9295e5f31a26c7
index 文件中显示的 flag 值:
000f
index 文件中显示的文件路径编码:
6c69 622f 7265 6164 6d65 322e 7478 7400
lib/readme2.txt
在系统中的路径编码值:
$ printf 'lib/readme2.txt' | xxd
00000000: 6c69 622f 7265 6164 6d65 322e 7478 74 lib/readme2.txt
使用 1-8 NUL 比特将 entry 填充为 8 byte 的倍数。
在index文件的extensions部分,每个扩展都由一个标识符和其对应的数据组成。扩展的标识符是一个四个字节的字符串,用于唯一标识扩展类型。在本 index 文件没有该类型,因此可以忽略
最末尾的 20 个字节的 index checksum,是由之前的 index 内容通过 SHA-1 算法计算得到的校验和。在本 index 文件中的编码是:
14da 2654 6c71 7806 f73b f4d8 0256 9c92 430e 1c
计算 SHA-1 值方法在深入剖析Git对象底层原理中提到过,就不多赘述了。
本文首先从概念上介绍了Git的三大区域:工作区、暂存区和版本库,并阐述了使用Git的标准工作流程。
然后详细说明了暂存区的定义、它相关的操作命令和优势,比如分离工作区和版本库,控制提交内容等。重点描述了暂存区的实现机制 - Index文件。Index文件记录了已暂存文件的元数据和校验信息,它建立了工作区文件和对象库对象之间的对应关系。
最后解读了Index文件各部分的数据结构,包括头部信息、文件条目结构(属性、对象ID等)、扩展和校验和等内容。通过示例说明了它们与文件实际属性的对应关系。
《Git 权威指南》
https://mincong.io/2018/04/28/git-index/
https://titangene.github.io/article/git-index.html