Git暂存区原理详解

Git暂存区原理详解

暂存区及index文件

我们知道,要向git版本库提交一个文件,需要先用git add .(或git add <文件名>)把该文件添加到暂存区,然后再用git commit -m "描述这次提交了什么"来提交到版本库。

本文主要讲什么是暂存区及暂存区工作原理,至于“为什么要有暂存区”这个问题,目前我只能说,暂存区的一个作用是,先把你想提交的文件单个添加到暂存区,然后一起提交,但它肯定不只这个功能,有时候遇到要解决某些问题貌似因为暂存区的存在,很方便解决。

什么是暂存区?

暂存区,英文叫staging area(查词典是“集结待命区”的意思,git add到暂存区确实就是集结待命,等待commit),也有人叫它缓存区(cache),或者index,暂存和缓存都是一个意思,在暂存区的文件都是等待提交(commmit)的文件。

但为什么又叫index呢?因为git add的数据其实是存放在.git/index文件中的,index有“索引”的意思,这个文件起名为index,估计也是“待commit的索引列表”的意思。

index文件是一个二进制文件,我们用git ls-files可以查看里面的内容,用git ls-files --stage会多显示一些信息。

从实例开始

# 创建一个文件夹并进入该文件夹
mkdir test-git && cd test-git

# 初始化该文件夹为git仓库
git init

# 创建一个readme.txt文件
echo 'This is a test' > readme.txt

# 在readme.txt文件未添加到暂存区时,先查看一下暂存区有什么(肯定是什么都没有)
git ls-files --stage

# 把刚刚创建的git add文件添加到暂存区中
git add readme.txt

# 然后再看看暂存区有什么
git ls-files --stage

git ls-files --stage输出如下

100644 0527e6bd2d76b45e2933183f1b506c7ac49f5872 0   readme.txt

输出内容的解释如下

类型及权限 二进制内容SHA-1值 暂存区编号 文件名
100644 0527e6bd2d76b45e2933183f1b506c7ac49f5872 0 readme.txt

在git源码说明中,有关于index文件的格式说明index-format.txt,这里面有讲这个二进制数字的意义

32-bit mode, split into (high to low bits)

4-bit object type
valid values in binary are 1000 (regular file), 1010 (symbolic link)
and 1110 (gitlink)

3-bit unused

9-bit unix permission. Only 0755 and 0644 are valid for regular files.
Symbolic links and gitlinks have value 0 in this field.

大概意思就是,这个index二进制文件中的二进制数字中,有32位二进制数字是被用来描述文件的类型以及权限的,虽然实际上只用了其中的16位,但是估计是为了统一,还是用了32位。并且描述了前4位、中间3位,后9位二进制数分别用来表示什么。

文件类型及权限

前面git ls-files --stage输出的100644这个数字就是用来表示该文件的类型及权限的,它其实是个八进制数(而不是十进制)。

100644这6个数字中的每一个数字,其实都是由3位二进制数转换成的,现在我们把它转回二进制

十进制 1 0 0 6 4 4
二进制 001 000 000 110 100 100

变成二进制就是:0b001000000110100100,其中0b开头的数表示是二进制数,第一个1前面两个0没有意义,可以不要,所以可以写成:0b1000000110100100。

我们来看一下这个二进制数,按前面说的分成4位+3位+9位三部分代表的意义

前四位 中间三位 最后9位
1000 000 110100100

前4位只可能有三种值:1000(普通文件)、1010(软链接文件)、1110(git链接文件)。
中间3位没有使用,应该是预留出来防止以后有用的。
最后9位是标准的权限位,前3位用来表示该文件所有者对它具有的权限、中间3位表示该文件所属组对它的权限、最后3位表示其它人(即非该文件所有者也非该文件所属组)对它的权限。

后9位分成3部分,如下所示

所有者 所属组 其它人
110 100 100

那为什么表示权限要用三位呢?因为这三位刚好代表了读、写、执行权限,它们的英文为read、write、execute,读、写都用首字母表示,执行用第二个字母x表示,估计是用e会跟其它的混淆吧。

如下所示,读(r)、写(w)、执行(x)分别对应一位二进制数,1表示具有该权限,0表示不具有该权限

r w x
1 1 0

所以,110就表示可读可写,但不可执行,而110转成八进制是6,所以一看到6,我们就应该知道它的权限为可读可写不可执行;同理,111就表示具有读写执行三个权限,转成八进制就是7,所以644是三个八进制数,并不是十进制数!

暂存区编号

暂存区中有四个分类,这些分类叫做slot(插槽),而git add添加的文件,就是插到这四个插槽中的,这四个插槽分别编号为:0、1、2、3。

Slot 0: “normal”, 无冲突的文件会添加到这里;
Slot 1: “base”, 共同的祖先版本;
Slot 2: “ours”, 目标(HEAD)版本;
Slot 3: “theirs”, 正在被合并的版本。

index文件内容构成详解

查看index二进制内容

使用xxd命令可以以二进制形式输出index文件中的内容

xxd -b .git/index

index文件二进制内容输出如下

00000000: 01000100 01001001 01010010 01000011 00000000 00000000  DIRC..
00000006: 00000000 00000010 00000000 00000000 00000000 00000001  ......
0000000c: 01100001 11111000 11000100 01011110 00100110 00000011  a..^&.
00000012: 10011010 10101010 01100001 11111000 11000100 01011110  ..a..^
00000018: 00100110 00000011 10011010 10101010 00000001 00000000  &.....
0000001e: 00000000 00000100 00001001 00101011 11111011 00000000  ...+..
00000024: 00000000 00000000 10000001 10100100 00000000 00000000  ......
0000002a: 00000001 11110101 00000000 00000000 00000000 00010100  ......
00000030: 00000000 00000000 00000000 00001111 00000101 00100111  .....'
00000036: 11100110 10111101 00101101 01110110 10110100 01011110  ..-v.^
0000003c: 00101001 00110011 00011000 00111111 00011011 01010000  )3.?.P
00000042: 01101100 01111010 11000100 10011111 01011000 01110010  lz..Xr
00000048: 00000000 00001010 01110010 01100101 01100001 01100100  ..read
0000004e: 01101101 01100101 00101110 01110100 01111000 01110100  me.txt
00000054: 00000000 00000000 00000000 00000000 00000000 00000000  ......
0000005a: 00000000 00000000 10000100 01001101 01111000 11000111  ...Mx.
00000060: 00100100 10001101 10100010 10011011 00000111 00000100  $.....
00000066: 10000100 10100010 01101111 11001111 11110110 01110001  ..o..q
0000006c: 00010011 11010110 11000110 01011100                    ...\

index文件的二进制存储格式在index-format.txt中有写,按顺序分别为

  • 1、12字节的头部信息(header),12字节是12×8=96位二进制;
  • 2、排好序的index文件名(就是git add加入暂存区的文件名);
  • 3、扩展信息,它们通过签名来识别(目前我不太清楚这是干什么用的);
  • 4、160位SHA-1校验和,对这个校验和前面的内容进行SHA-1计算得到的结果再拼凑到前面内容的尾部。

头部信息(header)

首先是12字节的头部信息,既然是“信息”,那它是什么信息呢?按顺序分别为:四个字节表示标记“DIRC”(DIR Cache的意思),四个字节表示版本号,最后四个字节表示暂存区文件数量。

字母 ASCII码(十进制) ASCII转二进制 二进制填充够8位
D 68 1000100 01000100
I 73 1001001 01001001
R 82 1010010 01010010
C 67 1000011 01000011

把填充够8位的二进制数全部排列在一起,我们会发现,它刚好就是前面输出的二进制内容的最前面的部分

01000100 01001001 01010010 01000011

如下图所示,红色的4个字节表示DIRC四个字母,绿色4个字节表示index文件的版本号(前面都是0,只有最后10是实际数字,10就是十进制的2),蓝色的4个字母表示暂存区文件数量(前面都是0只有最后一个是1,二进制1转成十进制也是1,表示当前暂存区只有一个文件)

文件创建及修改时间

header之后就是每个文件的信息了,因为我这边只git add了readme.txt,所以只有一个文件。

文件信息首先是64位的创建时间(create time, ctime),32位秒数+32位纳秒数;接着是64位的修改时间,同样是32位秒数+32位纳秒数。

首先我们用以下命令得到readme.txt文件的ctime和mtime

stat -f 'ctime=%c mtime=%m' readme.txt | tr ' ' '\n'

输出(由于我们只创建,未修改,所以修改时间与创建时间一样)

ctime=1643693150
mtime=1643693150

我们把1643693150(十进制)转成二进制为

1100001111110001100010001011110

该数为31位,我们在前面加个0,把它补齐32位,并按每8位一个空格隔开

01100001 11111000 11000100 01011110

如下图,第一个红色32位表示ctime(文件创建时间戳,秒数部分),第一个绿色32位表示ctime(文件创建时间戳纳秒部分),第二个红色表示mtime(文件修改时间戳,秒数部分),第二个绿色32闰表示mtime(文件修改时间戳,纳秒部分)

由于在macOS中貌似不记录文件创建时间的纳秒部分,所以我们只能验证秒数部分是对的上的。

dev设备信息

接下来是32位设备信息,这块我还没有验证到,不知道具体指什么设备,我找了一下/dev/下的硬盘设备,把设备名字转成二进制对不上,暂时放下,后面如果研究出来再补

00000001 00000000 00000000 00000100

inode编号

接下来就是32位的ino(inode)编号,inode是index node,索引节点,硬盘存储原理会用到这个

ls -i readme.txt

输出inode如下

153877248 readme.txt

153877248转成二进制并在前面补0补齐到32位,每8位隔开如下

00001001 00101011 11111011 00000000

32位文件类型及权限

接下来是32位的mode(文件类型+权限),前面我们已经查看过文件权限为:100644,刚好对应以下32位,其中前面16位都是0表示没有用到

00000000 00000000 10000001 10100100

32位uid

接下来是32位的uid,使用id -u可以查看当前用户id,对于macOS默认用户的id一般都是501,转成二进制是111110101,前面填充0补到32位,并每8位隔开

00000000 00000000 00000001 11110101

32位gid

接下来是32位gid,使用id -g可以获取,macOS一般是20,转成二进制是10100,在前面补0填充够32位,并每8位隔开为

00000000 00000000 00000000 00010100

32位文件大小

32位的文件大小,使用ls -l readme.txt可以查看文件大小,可以看到是15,单位是字节(Byte)

15转成二进制为“1111”,在前面补0补齐32位并每8位隔开为

00000000 0000000 00000000 00001111

160位SHA-1值

160位(即20Byte)SHA-1值,这个值就是前面我们用看到的那串长度为40的十六进制数

git ls-files readme.txt --stage

我们把“0527e6bd2d76b45e2933183f1b506c7ac49f5872”转成二进制数有点麻烦,我们可以反过来转,就是把二进制转成16进制,如下所示

上图一共20个字节,每个字节有8位,4位二进制就可以转一位十六进制,所以一个字节前4位跟后4位分开转就行,比如最后一个8位是“01110010”,那么前四位转十六进制数是7,后四位是2,所以转成十六进制就是72(注意这是十六进制,不是十进制)。

转换结果,刚好就是:

0527e6bd2d76b45e2933183f1b506c7ac49f5872

其实当进制数=2n时,它们与二进制是可以直接相互转换的,不用除2取余法,比如16进制,我们直接把它的每位数字转成二进制就行,不足4位在前面补0,而且一定要补0,八进制就是每3位二进制表示一位八进制数,也是不足3位就补0。

根据这个原理,我用golang写了个十六进制转二进制字符串的转换函数

// Hex2binStr converts hexdecimal to binary number and return as string
func Hex2binStr(hex string) (binStr string) {
    hex2binMap := map[string]string{
        "0": "0000",
        "1": "0001",
        "2": "0010",
        "3": "0011",
        "4": "0100",
        "5": "0101",
        "6": "0110",
        "7": "0111",
        "8": "1000",
        "9": "1001",
        "a": "1010",
        "b": "1011",
        "c": "1100",
        "d": "1101",
        "e": "1110",
        "f": "1111",
    }

    charCount := len(hex)
    for i := 0; i < charCount; i++ {
        if bin, ok := hex2binMap[hex[i:i+1]]; ok {
            binStr += bin
        }
    }
    return binStr
}

16位flag

接下来是16位的flag

  • 1位假设有效标志,在这里是0
  • 1位扩展标志,在index版本2中必须为0(前面的header里已经得知我目前用的git的index版本是2)
  • 2位stage(在合并期间使用),其实具体怎么用我也不知道
  • 12位文件名称长度(如果长度小于0xFFF的话,如果大于0xFFF,那就是0xFFF,因为12位能存下的最大数就是0xFFF,其中0x表示十六进制)。

16位flag如下

n位文件名

接下来是文件名,readme.txt共10个字符,每个字符1字节,共10字节,所以文件名占多少位要看文件名的长短

NUL字节结尾

文件名结束后,实际内容已经结束了,但是它要求文件名一定要以NUL结尾(可能是为了识别结尾吧),注意nul是名词,意思就是“空;空字符”,而null是形容词,表示空的,零值。

所以我们需要填充一个字节的空值,也就是8个全0,但是这样还不行,因为还要求从header之后(其实就是ctime开始)到文件名NUL字节结尾总字节数必须为8的倍数。

我们先看一下,从ctime到文件名(此时未添加NUL结尾)共12行,每行6个字节就是72,72刚好是8的倍数,但是如果我添加一个NUL字节后,就不是8的倍数了,这意味着,我必须一次性添加8个NUL能凑够8的倍数

所以这后面8个NUL(全0)字节,是为了凑够从header后到NUL结尾总字节数为8的倍数

假如有另外的例子,比如从header后到文件名结尾有75个字节,那么再加一个NUL字节就是76字节,很显示我们还要额外加4个全0(NUL)字节凑够80,这样才是8的倍数,所以这个末尾补多少个NUL字节是不一定的,主要是为了保证:以NUL字节结尾+从header后到结尾的NUL字节之间的总字节数是8的倍数。

160位总SHA-1值

最后20个字节又是检验和(checksum),它的值是这个校验和前面所有内容做SHA-1算法获得的

这篇文章讲的非常详细。

打赏
订阅评论
提醒
guest

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据

2 评论
内联反馈
查看所有评论
pony
pony
2 年 前

题主写的真好,赞

2
0
希望看到您的想法,请您发表评论x

扫码在手机查看
iPhone请用自带相机扫
安卓用UC/QQ浏览器扫

Git暂存区原理详解