5.2 理解Git暂存区(stage)

将上面的实践从头至尾操作一遍,不知道您的感想如何:

——“被眼花缭乱的Git魔法彻底搞糊涂了?”

——“Git为什么这么折磨人,修改的文件直接提交不就完了吗?”

——“看不出Git这么做有什么好处?”

上面的实践过程有意无意地透漏了“暂存区”的概念。为了避免用户被新概念吓坏,在暂存区出现的地方又同时使用了“提交任务”这一更易理解的概念,但是暂存区(称为stage或index)才是其真正的名称。我认为Git暂存区的设计是Git最成功的设计之一,但也是最难理解的。

在版本库.git目录下有一个index文件,下面针对这个文件做一个有趣的试验。要说明的是:这个试验是用1.7.3版本的Git进行的,低版本的Git因为没有针对git status命令进行优化设计,需要运行git diff命令才能看到index文件的日期戳变化,具体操作步骤如下。

(1)首先执行git checkout命令(后面会介绍此命令),撤销工作区中welcome.txt文件尚未提交的修改。


$git checkout—welcome.txt

$git status-s#执行git diff,如果git版本号小于1.7.3


(2)通过状态输出可以看到工作区已经没有改动了。查看一下.git/index文件,注意该文件的时间戳为:19:37:44。


$ls—full-time.git/index

-rw-r—r—1 jiangxin jiangxin 112 2010-11-29 19:37:44.625246224+0800.git/index


(3)再次执行git status命令,然后显示.git/index文件的时间戳为:19:37:44,与上面的一样。


$git status-s#执行git diff,如果git版本号小于1.7.3

$ls—full-time.git/index

-rw-r—r—1 jiangxin jiangxin 112 2010-11-29 19:37:44.625246224+0800.git/index


(4)现在更改一下welcome.txt的时间戳,但是不改变它的内容。然后再执行git status命令,查看.git/index文件的时间戳为:19:42:06。


$touch welcome.txt

$git status-s#执行git diff,如果git版本号小于1.7.3

$ls—full-time.git/index

-rw-r—r—1 jiangxin jiangxin 112 2010-11-29 19:42:06.980243216+0800.git/index


看到了吗,时间戳改变了!

这个试验说明当执行git status命令(或者git diff命令)扫描工作区改动的时候,先依据.git/index文件中记录的(用于跟踪工作区文件的)时间戳、长度等信息判断工作区文件是否改变,如果工作区文件的时间戳改变了,说明文件的内容可能被改变了,需要打开文件,读取文件内容,与更改前的原始文件相比较,判断文件内容是否被更改。如果文件内容没有改变,则将该文件新的时间戳记录到.git/index文件中。因为如果要判断文件是否更改,使用时间戳、文件长度等信息进行比较要比通过文件内容比较要快得多,所以Git这样的实现方式可以让工作区状态扫描更快速地执行,这也是Git高效的原因之一。

文件.git/index实际上就是一个包含文件索引的目录树,像是一个虚拟的工作区。在这个虚拟工作区的目录树中,记录了文件名和文件的状态信息(时间戳和文件长度等)。文件的内容并没有存储在其中,而是保存在Git对象库.git/objects目录中,文件索引建立了文件和对象库中对象实体之间的对应。图5-1展示了工作区、版本库中的暂存区和版本库之间的关系。

5.2 理解Git暂存区(stage) - 图1

图 5-1 工作区、版本库、暂存区原理图

从图5-1中可以看到部分Git命令是如何影响工作区和暂存区的。这些命令的面纱将在接下来的几个章节中彻底揭开,下面就对这些命令进行简要说明:

图中左侧为工作区,右侧为版本库。在版本库中标记为index的区域是暂存区,标记为master的是master分支所代表的目录树。

图中可以看出,此时HEAD实际是指向master分支的一个“游标”,所以图示的命令中出现HEAD的地方可以用master来替换。

图中的objects标识的区域为Git的对象库,实际位于.git/objects目录下,这一点会在后面的章节中重点介绍。

当对工作区修改(或新增)的文件执行git add命令时,暂存区的目录树将被更新,同时工作区修改(或新增)的文件内容会被写入到对象库中的一个新的对象中,而该对象的ID被记录在暂存区的文件索引中。

当执行提交操作(git commit)时,暂存区的目录树会写到版本库(对象库)中,master分支会做相应的更新,即master最新指向的目录树就是提交时原暂存区的目录树。

当执行git reset HEAD命令时,暂存区的目录树会被重写,会被master分支指向的目录树所替换,但是工作区不受影响。

当执行git rm—cached<file>命令时,会直接从暂存区删除文件,工作区则不做出改变。

当执行git checkout.或git checkout—<file>命令时,会用暂存区全部的文件或指定的文件替换工作区的文件。这个操作很危险,会清除工作区中未添加到暂存区的改动。

当执行git checkout HEAD.或git checkout HEAD<file>命令时,会用HEAD指向的master分支中的全部或部分文件替换暂存区和工作区中的文件。这个命令也是极具危险性的,因为不但会清除工作区中未提交的改动,也会清除暂存区中未提交的改动。