暂存区并不是一个树对象。也就是说,你不能用 git ls-tree 迭代其内容。然而,它确实指向对象数据库中的blob。那么,为什么我们需要一种不同类型的对象来引用暂存区?
其中一些原因是出于性能考虑。每当进行差异(或其他整个仓库的操作)时,Git 需要快速而高效地计算工作树的状态是否自上次暂存区以来发生了变化。一些工具,包括 bash shell 提示符,需要能够迅速确定工作树是否处于脏状态:
1 | (master) $ git status |
如果 Git 只能通过对内容进行完整遍历(并计算它们的 SHA1 哈希)来判断树的状态是否脏,这个操作将变得代价高昂。幸运的是,Git 有许多优化措施,使其能够避免这种情况。
暂存区不仅存储文件名,还存储这些文件的最后修改时间。因此,Git 可以通过迭代文件的元数据并将时间戳与暂存区中的时间戳进行比较来判断是否有任何时间戳的更改。如果文件在暂存区中缺失,它被表示为添加。如果文件在磁盘上缺失,它被表示为删除。如果文件的修改时间不同,则被表示为修改。
除了存储时间戳,暂存区还存储每个 blob 的 SHA1 哈希。这使得暂存区可以自行更新,如果文件被还原到先前的状态但时间戳较晚。
最后,暂存区还用于处理合并。在暂存区中,有一个拥有多个暂存区号(或阶段号)的概念。通常,只使用 0,因为这表示当前工作树的状态。然而,如果出现合并冲突,暂存区将用于消除文件在每个级别上的状态的歧义。如果存在冲突,则阶段 0 用于表示当前工作树,阶段 1 用于表示你的更改,然后阶段 2 和 3 用于其他差异。你可以通过运行 git ls-files --stage
(或 -s)来查看阶段号:
1 | (master) $ git status |
这里的阶段 1 文件是这两个文件派生的地方,因此我们可以进行 1..2 和 1..3 的差异,以找出每一侧的树自那时以来发生了什么变化。(你们中更敏锐的人会认识到 e69…391 是空文件。)我们可以显示这些文件版本的内容,或者将它们加载到三向差异工具中,如果你有这样的工具:
1 | (master) $ git show e69de #empty file |
当然,通常 Git 会为你处理差异,你不需要担心具体的更改,也不需要从暂存区中提取内容。但这确实突显了一个事实,即当你完成对单个文件的合并时,在文件上运行 git add
将在暂存区的阶段 0 中放置一个副本,删除了 1、2、3 的暂存区:
1 |
|
这就是为什么使用 Git 合并大的有冲突的更改变得容易的原因。每个文件都可以按照文件的方式进行处理;当你完成合并一个文件时,你可以将它添加到暂存区中,这既记录了它的内容,又删除了其他文件的合并状态。然后,合并许多文件就变成了逐个合并它们并在进行过程中添加它们的过程。而且,由于它们都暂存在暂存区中,你可以一直添加它们,直到准备执行 git commit
为止。