git:深入了解暂存区

暂存区并不是一个树对象。也就是说,你不能用 git ls-tree 迭代其内容。然而,它确实指向对象数据库中的blob。那么,为什么我们需要一种不同类型的对象来引用暂存区?

其中一些原因是出于性能考虑。每当进行差异(或其他整个仓库的操作)时,Git 需要快速而高效地计算工作树的状态是否自上次暂存区以来发生了变化。一些工具,包括 bash shell 提示符,需要能够迅速确定工作树是否处于脏状态:

1
2
3
4
5
6
7
8
9
10
11
(master) $ git status
# On branch master
# Changes not staged for commit:
# (use "git add <file>…" to update what will be committed)
# (use "git checkout -- <file>…" to discard changes in working directory)
#
# modified: example
#
no changes added to commit (use "git add" and/or "git commit -a")
(master) $ export GIT_PS1_SHOWDIRTYSTATE=true
(master *) $

如果 Git 只能通过对内容进行完整遍历(并计算它们的 SHA1 哈希)来判断树的状态是否脏,这个操作将变得代价高昂。幸运的是,Git 有许多优化措施,使其能够避免这种情况。

暂存区不仅存储文件名,还存储这些文件的最后修改时间。因此,Git 可以通过迭代文件的元数据并将时间戳与暂存区中的时间戳进行比较来判断是否有任何时间戳的更改。如果文件在暂存区中缺失,它被表示为添加。如果文件在磁盘上缺失,它被表示为删除。如果文件的修改时间不同,则被表示为修改。

除了存储时间戳,暂存区还存储每个 blob 的 SHA1 哈希。这使得暂存区可以自行更新,如果文件被还原到先前的状态但时间戳较晚。

最后,暂存区还用于处理合并。在暂存区中,有一个拥有多个暂存区号(或阶段号)的概念。通常,只使用 0,因为这表示当前工作树的状态。然而,如果出现合并冲突,暂存区将用于消除文件在每个级别上的状态的歧义。如果存在冲突,则阶段 0 用于表示当前工作树,阶段 1 用于表示你的更改,然后阶段 2 和 3 用于其他差异。你可以通过运行 git ls-files --stage(或 -s)来查看阶段号:

1
2
3
4
5
6
7
8
9
10
11
(master) $ git status
(master) $ git ls-files -s
100644 ce013625030ba8dba906f756967f9e9ca394464a 0 example
(master) $ git pull # with known conflict
Auto-merging example
CONFLICT (content): Merge conflict in example
Automatic merge failed; fix conflicts and then commit the result.
(master|MERGING) $ git ls-files -s
100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 1 example
100644 a895c0db0a627cb9451ae390a2a0922495dbb161 2 example
100644 13940d48c3d3693113f7543d4fc5423a916ef55d 3 example

这里的阶段 1 文件是这两个文件派生的地方,因此我们可以进行 1..2 和 1..3 的差异,以找出每一侧的树自那时以来发生了什么变化。(你们中更敏锐的人会认识到 e69…391 是空文件。)我们可以显示这些文件版本的内容,或者将它们加载到三向差异工具中,如果你有这样的工具:

1
2
3
4
5
(master) $ git show e69de #empty file
(master) $ git show a895c
Left tree
(master) $ git show 13940
Right tree

当然,通常 Git 会为你处理差异,你不需要担心具体的更改,也不需要从暂存区中提取内容。但这确实突显了一个事实,即当你完成对单个文件的合并时,在文件上运行 git add 将在暂存区的阶段 0 中放置一个副本,删除了 1、2、3 的暂存区:

1
2
3
4

(master|MERGING) $ git add example
(master|MERGING) $ git ls-files -s
100644 319c128291474d30f48e721ca87bd10425e8e296 0 example

这就是为什么使用 Git 合并大的有冲突的更改变得容易的原因。每个文件都可以按照文件的方式进行处理;当你完成合并一个文件时,你可以将它添加到暂存区中,这既记录了它的内容,又删除了其他文件的合并状态。然后,合并许多文件就变成了逐个合并它们并在进行过程中添加它们的过程。而且,由于它们都暂存在暂存区中,你可以一直添加它们,直到准备执行 git commit 为止。