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文件中的内容,判断文件是否添加到暂存区中。

powershell 复制代码
//创建一个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目录中,如下图所示:

  1. git add <文件名>:将指定文件从工作区添加到暂存区。暂存区的目录树将会被更新,文件内容会被写入到对象库中的一个新对象中,该对象的 ID 将会被记录在暂存区也就是 Index 文件中。
  2. git restore --staged <文件名>:将指定文件或修改从暂存区移除,但保留在工作区中。这个命令会撤销之前使用 git add 命令添加到暂存区的文件或修改。
  3. git checkout -- <文件名>:撤销对指定文件的修改,将其恢复到最近一次提交的状态。这个命令会影响工作区中的文件,但不会改变暂存区。
  4. git checkout HEAD : 会用 HEAD 指向的 master 分支中的全部文件替换暂存区和工作区的文件
  5. git reset HEAD <文件名>:将指定文件从暂存区移除,但保留在工作区中。这个命令可以用于撤销之前使用git add命令添加到暂存区的文件。
  6. git commit:将暂存区的内容作为一个整体提交到版本库中。这个命令会将暂存区中的文件和修改保存到版本库中,不会直接影响工作区和暂存区。master 最新指向的目录树就是提交时原暂存区的目录树。
  7. git rm --cached <文件名>: 直接将暂存区的该文件删除,工作区则不做出改变

2.3 Git暂存区的优势和应用场景

  1. 分离工作目录和版本库:工作目录是你实际工作的地方,你可以在其中进行任意修改和调整。但并不是所有的修改都应该立即提交到版本库中。通过使用暂存区,你可以将对工作目录的修改与版本库的提交操作分离开来,从而更好地管理你的项目。
  2. 控制提交的内容:暂存区允许你选择性地添加修改到提交中。当你在工作目录中对多个文件进行修改时,你可以根据需要选择性地将这些修改添加到暂存区。这使得你可以按照逻辑和功能进行修改的分组,而不是一次性提交所有的修改。这样,你可以更好地组织和控制提交的内容。
  3. 检查和确认修改:通过将修改添加到暂存区,你可以在提交之前对这些修改进行检查和确认。你可以使用git status命令查看暂存区中的修改,以确保包含了你想要提交的所有内容。这使得你可以在提交之前进行代码审查、测试和调整,以确保提交的内容是正确的和完整的。
  4. 多次提交:使用暂存区,你可以将修改分批提交到版本库中。你可以多次添加修改到暂存区,然后在适当的时候执行提交操作。这样,你可以将修改的提交分散到多个较小的提交中,从而更好地跟踪项目的演变和历史记录。

三、Index文件:暂存区的实现机制

3.1 Index文件是什么

Index 文件是一个二进制文件,位于 Git 仓库的 .git 目录下,具体路径是 .git/index。它是一个索引文件,里面包含了一系列的记录条目。每个记录条目对应一个被跟踪的文件,记录了文件的元数据和状态信息。 此外 Index 建立工作目录中的文件和对象库中对象实体之间的对应关系。其与工作目录,版本库的关系可以继续引用上一节的图: 前面提到过可以使用 git ls-files命令来查看 index 文件,它主要有两个参数:

  • -c-cached:默认选项,只显示已经暂存的文件名
  • -s--stage:除了暂存的文件名,还有模式,暂存区编号等等信息

比如上一节中的 index 文件输入如下:

shell 复制代码
$ 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 文件内容:

shell 复制代码
$ 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 文件由以下内容组成:

  1. 12 字节的 header 头部
  2. 多个排序的 index entries,也就是加入暂存区的文件信息
  3. extensions,通过签名来识别
  4. sha-1 Index Checksum,160 位 SHA-1 的校验和

3.2.1 Header

Index的开头部分包含一个固定的头部,其中包含了签名,版本号、索引记录的总条目数: 从我们上面输出的 index 文件内容,取前 12 字节,可以知道其 Header 的内容为:

shell 复制代码
00000000: 4449 5243 0000 0002 0000 0003
  1. 签名(Signature):索引头部的前4个字节是一个固定的签名,用于标识文件的类型。在当前版本的Git中,签名的内容是 4449 5243对应的是DIRC。
  2. 版本号(Version):紧随签名之后的4个字节表示索引的版本号。不同版本的Git可能会有不同的索引版本格式。目前使用的版本号内容是 0000 0002对应内容是2。
  3. 条目数(Entry Count):接下来的4个字节表示索引中的文件条目数。它指示了索引中有多少个文件索引记录(Index Entry)。当前版本中的条目数是 0000 0003对应条目 3 条

我们再来看看实际 index 中的文件数:

shell 复制代码
$ 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文件

shell 复制代码
65a7 3bef 2de3 5dc8 65aa 2362 3187 05b0

可以利用 stat来查看lib/readme2.txt文件的更改时间和文件内容的修改时间:

shell 复制代码
//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 文件下的值:

shell 复制代码
0000 0000

inode 编号信息,共 4 字节,表示 index node,索引节点

shell 复制代码
0000 0000

文件类型和权限(共 4 字节)

也就是文件类型和权限,当前文件下是普通文件:100644,对应的也就是

shell 复制代码
0000 81a4

前面的 16 位为 0,查看系统中的文件,与 index 中编码相符

shell 复制代码
$ stat -c '%f' lib/readme2.txt
81a4

用户和组信息(uid + gid 共计 8 字节)

  • uid:当前用户的 user identifier:
shell 复制代码
0000 0000
  • gid:当前用户的 group identifier:
shell 复制代码
0000 0000

文件大小(file size 共计 4 字节)

index 文件中的编码,显示的文件大小

shell 复制代码
0000 0006

可以用 stat来查看系统中文件大小:

shell 复制代码
$ stat -c '%s' lib/readme2.txt
6

文件对象 ID 值(SHA-1 20 字节)

index 文件中显示的 ID 值:

shell 复制代码
4632 e068 d588 9f04 2fe2 d925 4a92 95e5 f31a 26c7

可以使用 git hash-object来查看文件的 ID 值:

shell 复制代码
$ git hash-object lib/readme2.txt 
4632e068d5889f042fe2d9254a9295e5f31a26c7

flag 值(共计 2 字节)

index 文件中显示的 flag 值:

shell 复制代码
000f
  • 1-bit:assume-valid flag
  • 1-bit:extended flag (在版本 2 中必须为零)
  • 2-bit:stage (合并期间)
  • 如果 length 小于 0xFFF,则为 12-bit name length;否则,将 0xFFF 储存在此 field 中

文件路径

index 文件中显示的文件路径编码:

shell 复制代码
6c69 622f 7265 6164 6d65 322e 7478 7400

lib/readme2.txt 在系统中的路径编码值:

shell 复制代码
$ 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 文件中的编码是:

shell 复制代码
14da 2654 6c71 7806 f73b f4d8 0256 9c92 430e 1c

计算 SHA-1 值方法在深入剖析Git对象底层原理中提到过,就不多赘述了。

四、总结

本文首先从概念上介绍了Git的三大区域:工作区、暂存区和版本库,并阐述了使用Git的标准工作流程。 然后详细说明了暂存区的定义、它相关的操作命令和优势,比如分离工作区和版本库,控制提交内容等。重点描述了暂存区的实现机制 - Index文件。Index文件记录了已暂存文件的元数据和校验信息,它建立了工作区文件和对象库对象之间的对应关系。 最后解读了Index文件各部分的数据结构,包括头部信息、文件条目结构(属性、对象ID等)、扩展和校验和等内容。通过示例说明了它们与文件实际属性的对应关系。

参考资料

《Git 权威指南》 mincong.io/2018/04/28/... titangene.github.io/article/git...

相关推荐
GoppViper33 分钟前
golang学习笔记29——golang 中如何将 GitHub 最新提交的版本设置为 v1.0.0
笔记·git·后端·学习·golang·github·源代码管理
m0_464832362 小时前
Linux服务器上安装git lfs命令
git
贩卖纯净水.9 小时前
白月光git
git·github
爱吃瓜的猹z13 小时前
git reset 几点疑问
git·源代码管理
悟空201619 小时前
001、Git开发流程规范
git
Li小李同学Li20 小时前
git学习【持续更新中。。。】
git·学习·elasticsearch
晨春计21 小时前
【git】
android·linux·git
念幽1 天前
Git常用命令
git
神技圈子1 天前
【git系列】git中的那些迷惑的术语以及概念详解
git
benben0441 天前
Photoshop使用方法大全
git