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 权威指南》
https://mincong.io/2018/04/28/git-index/
https://titangene.github.io/article/git-index.html

相关推荐
paopaokaka_luck2 小时前
【360】基于springboot的志愿服务管理系统
java·spring boot·后端·spring·毕业设计
逐·風3 小时前
unity关于自定义渲染、内存管理、性能调优、复杂物理模拟、并行计算以及插件开发
前端·unity·c#
Devil枫4 小时前
Vue 3 单元测试与E2E测试
前端·vue.js·单元测试
码农小旋风4 小时前
详解K8S--声明式API
后端
Peter_chq4 小时前
【操作系统】基于环形队列的生产消费模型
linux·c语言·开发语言·c++·后端
Yaml44 小时前
Spring Boot 与 Vue 共筑二手书籍交易卓越平台
java·spring boot·后端·mysql·spring·vue·二手书籍
小小小妮子~4 小时前
Spring Boot详解:从入门到精通
java·spring boot·后端
hong1616884 小时前
Spring Boot中实现多数据源连接和切换的方案
java·spring boot·后端
尚梦4 小时前
uni-app 封装刘海状态栏(适用小程序, h5, 头条小程序)
前端·小程序·uni-app
GIS程序媛—椰子5 小时前
【Vue 全家桶】6、vue-router 路由(更新中)
前端·vue.js