Git 暂存区机制详解
Git的暂存区(Staging Area)是位于Git仓库内部的一个中间区域,也就是版本库.git目录下的index文件。暂存区的含义是,在对项目文件进行修改后,这些修改并不会立即被提交到版本库中。相反,你需要将这些修改先添加到暂存区,然后才能将其作为一个整体提交到版本库中。本文首先从概念上介绍了Git的三大区域:工作区、暂存区和版本库,并阐述了使用Git的标准工作流程。然后详细说明了暂存区的定义
一、引言
从git init 执行后发生了什么我们知道,Git 经过初始化后,会形成三个主要区域:工作目录(Working Directory)、暂存区(Staging Area)和版本库(Repository):
- **工作目录(Working Directory)**是在计算机上实际工作的目录,其中包含了项目文件。当对项目文件进行修改时,这些修改只存在于工作目录中,并没有被Git跟踪。
- **暂存区(Staging Area)**是位于Git仓库内部的一个中间区域。它相当于一个缓冲区,用于存储想要提交到版本库的修改。当对项目文件进行修改后,需要将这些修改添加到暂存区,以便在下一次提交时包含这些修改。
- **版本库(Repository)**是Git的核心部分,它保存了项目的完整历史记录。版本库由一系列的提交(Commits)组成,每个提交代表一个特定时间点上的项目状态。当执行提交操作时,Git会将暂存区中的内容创建为一个新的提交,并将其添加到版本库中。
使用 Git 的工作流程是:先将文件放入工作目录中,然后使用 git add <文件名>
命令将该文件添加到暂存区,接着使用 git commit
提交到版本库中 。
下面就具体看看暂存区的机制和原理吧
二、Git暂存区的定义和操作
Git的暂存区(Staging Area)是位于Git仓库内部的一个中间区域,也就是版本库 .git
目录下的 index
文件。暂存区的含义是,在对项目文件进行修改后,这些修改并不会立即被提交到版本库中。相反,你需要将这些修改先添加到暂存区,然后才能将其作为一个整体提交到版本库中。
2.1 如何将修改的文件添加到暂存区
要将修改的文件添加到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
,此时进入交互式的模式,让你逐个选择要添加的文件。你可以根据提示进行操作,选择要添加的文件并确认操作:
2.2 Git 命令如何影响工作目录和暂存区
.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 <文件名>
: 直接将暂存区的该文件删除,工作区则不做出改变
2.3 Git暂存区的优势和应用场景
- 分离工作目录和版本库:工作目录是你实际工作的地方,你可以在其中进行任意修改和调整。但并不是所有的修改都应该立即提交到版本库中。通过使用暂存区,你可以将对工作目录的修改与版本库的提交操作分离开来,从而更好地管理你的项目。
- 控制提交的内容:暂存区允许你选择性地添加修改到提交中。当你在工作目录中对多个文件进行修改时,你可以根据需要选择性地将这些修改添加到暂存区。这使得你可以按照逻辑和功能进行修改的分组,而不是一次性提交所有的修改。这样,你可以更好地组织和控制提交的内容。
- 检查和确认修改:通过将修改添加到暂存区,你可以在提交之前对这些修改进行检查和确认。你可以使用git status命令查看暂存区中的修改,以确保包含了你想要提交的所有内容。这使得你可以在提交之前进行代码审查、测试和调整,以确保提交的内容是正确的和完整的。
- 多次提交:使用暂存区,你可以将修改分批提交到版本库中。你可以多次添加修改到暂存区,然后在适当的时候执行提交操作。这样,你可以将修改的提交分散到多个较小的提交中,从而更好地跟踪项目的演变和历史记录。
三、Index文件:暂存区的实现机制
3.1 Index文件是什么
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
- 100644:mode, 代表类型和权限。在 unix 中常见的模式有:
040000
:目录100644
:普通文件。用于存储文本、二进制数据或程序代码等100664
:普通文件,和 100644 的区别在于它具有默认的文件权限,即所有者具有读取和写入权限,而组和其他用户只有读取权限。100755
:可执行文件,具有可执行权限的文件,通常是二进制可执行程序或脚本文件120000
:符号链接,指向另一个文件或目录的特殊文件类型160000
:Git 子模块是引用其他 Git 仓库的特殊文件类型。它允许将一个 Git 仓库作为另一个仓库的子目录进行管理
- e69de29bb2d… :代表文件的 SHA-1 值
- 0:stage number, 代表暂存区编号,用于处理合并冲突。主要有这样几个值:
- 0:normal, 无冲突,一切正常
- 1:base, 用于标识冲突的文件,指冲突分支的共同祖先版本
- 2:ours,目标分支的版本(通常是当前分支),在冲突解决期间,Git 会将当前分支的版本放在2号暂存区。
- 3:theris, 区分于当前分支,是正在合并的分支的版本,在冲突解决期间,Git 会将合并分支的版本放在3号暂存区。
- lib/readme2.txt:文件路径,此时 readme2.txt 位置在工作目录 lib 中
3.2 Index 文件的内容和数据结构
我们可以用 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 文件由以下内容组成:
- 12 字节的 header 头部
- 多个排序的 index entries,也就是加入暂存区的文件信息
- extensions,通过签名来识别
- sha-1 Index Checksum,160 位 SHA-1 的校验和
3.2.1 Header
Index的开头部分包含一个固定的头部,其中包含了签名,版本号、索引记录的总条目数:
从我们上面输出的 index 文件内容,取前 12 字节,可以知道其 Header 的内容为:
00000000: 4449 5243 0000 0002 0000 0003
- 签名(Signature):索引头部的前4个字节是一个固定的签名,用于标识文件的类型。在当前版本的Git中,签名的内容是
4449 5243
对应的是DIRC。 - 版本号(Version):紧随签名之后的4个字节表示索引的版本号。不同版本的Git可能会有不同的索引版本格式。目前使用的版本号内容是
0000 0002
对应内容是2。 - 条目数(Entry Count):接下来的4个字节表示索引中的文件条目数。它指示了索引中有多少个文件索引记录(Index Entry)。当前版本中的条目数是
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
3.2.2 Index entry
文件索引记录(Index Entry)是Git索引(Index)中的关键部分,用于表示 Index 中的每个文件。每个文件索引记录包含了文件的修改时间,文件模式,文件名,对象 ID 等等信息,下面仍然以实验中用的 index 文件来讲述其不同部分的组成:
文件时间信息(btime 和 mtime 共计 16 字节)
- btime 指 brithday time,文件的创建时间。由 4 字节秒数+4 字节纳秒数组成
- mtime 指 modify time,文件的修改时间,文件内容的最后一次修改时间。由 4 字节秒数+4 字节纳秒数组成
先来看看对应上面二进制 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 设备信息,inode 编号(共计 8 字节)
dev 设备信息,共 4 字节,表示文件对应的设备信息,在本 index 文件下的值:
0000 0000
inode 编号信息,共 4 字节,表示 index node,索引节点
0000 0000
文件类型和权限(共 4 字节)
也就是文件类型和权限,当前文件下是普通文件:100644,对应的也就是
0000 81a4
前面的 16 位为 0,查看系统中的文件,与 index 中编码相符
$ stat -c '%f' lib/readme2.txt
81a4
用户和组信息(uid + gid 共计 8 字节)
- uid:当前用户的 user identifier:
0000 0000
- gid:当前用户的 group identifier:
0000 0000
文件大小(file size 共计 4 字节)
index 文件中的编码,显示的文件大小
0000 0006
可以用 stat
来查看系统中文件大小:
$ stat -c '%s' lib/readme2.txt
6
文件对象 ID 值(SHA-1 20 字节)
index 文件中显示的 ID 值:
4632 e068 d588 9f04 2fe2 d925 4a92 95e5 f31a 26c7
可以使用 git hash-object
来查看文件的 ID 值:
$ git hash-object lib/readme2.txt
4632e068d5889f042fe2d9254a9295e5f31a26c7
flag 值(共计 2 字节)
index 文件中显示的 flag 值:
000f
- 1-bit:assume-valid flag
- 1-bit:extended flag (在版本 2 中必须为零)
- 2-bit:stage (合并期间)
- 如果 length 小于 0xFFF,则为 12-bit name length;否则,将 0xFFF 储存在此 field 中
文件路径
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
Null 位填充(1-8 字节)
使用 1-8 NUL 比特将 entry 填充为 8 byte 的倍数。
3.2.3 Extensions
在index文件的extensions部分,每个扩展都由一个标识符和其对应的数据组成。扩展的标识符是一个四个字节的字符串,用于唯一标识扩展类型。在本 index 文件没有该类型,因此可以忽略
3.2.4 SHA-1 Index checksum
最末尾的 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
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)