它几乎是对称的,但并非完全对称。
git add file
确实将文件复制到暂存区(也称为“索引”)。 但是,它所做的方式有点奇怪。
在git库中,所有内容都以git“对象”的形式存储。每个对象都有一个唯一的名称,即其SHA-1(那些40个字符的字符串,例如753be37fca1ed9b0f9267273b82881f8765d6b23
——这是我这里实际使用的.gitignore
文件的哈希值)。名称是通过计算文件内容的哈希来构建的(或多或少——有些花招可以确保您不会从目录树或提交中创建文件,并导致哈希冲突)。 Git认为,无论内容如何,SHA-1都是唯一的:不同的文件、目录树、提交或注释标记永远不会哈希到相同的值。
文件(和符号链接)是类型为“blob”的对象。因此,位于git仓库中的文件被哈希,而且git某处有一个映射:“名为.gitignore
的文件”到“哈希值753be37fca1ed9b0f9267273b82881f8765d6b23
”。
在仓库中,目录树被存储为类型为“tree”的对象。树对象包含名称列表(如.gitignore
)、模式、对象类型(另一个树或blob)和SHA-1值:
$ git cat-file -p HEAD:
100644 blob 753be37fca1ed9b0f9267273b82881f8765d6b23 .gitignore
[snip]
一个提交对象可以让您(或Git)获得一个树对象,最终可以获得blob ID。
另一方面,暂存区("索引")只是一个文件,.git/index
。该文件包含1名称(以一种有趣的稍微压缩的形式展平目录树),在合并冲突的情况下是“阶段号”,以及SHA-1。实际的文件内容再次是git repo中的blob。(Git不会在索引中存储目录:索引仅具有实际文件,使用该展平格式。)
因此,当您执行以下操作时:
git add file_name
Git会这样做(更多或更少,我故意省略了过滤器):
1. 计算文件file_name的内容哈希值(git hash-object -t blob)。
2. 如果该对象尚未存在于存储库中,则将其写入存储库(使用hash-object的-w选项)。
3. 更新.git / index(或$ GIT_INDEX_FILE),使其具有在名称file_name下的映射,以便它具有从git hash-object中得到的名称。这始终是一个“stage 0”条目(这是正常的,没有合并冲突的版本)。
因此,文件实际上并不在暂存区中,而是在存储库本身中!在暂存区中的是名称到SHA-1的映射关系。
相比之下,git checkout [] -- file_name执行以下操作:
1. 如果给定了(提交名称、树对象ID等,基本上是git可以解析为树的任何东西),则从转换参数为树对象找到的树中进行名称查找。使用找到的对象ID更新索引中的哈希值,作为阶段0。(如果file_name命名一个树对象,则git递归处理树所表示的目录中的所有文件。)通过创建阶段0条目,现在解决了file_name上的任何合并冲突。
否则,在索引中进行名称查找(如果file_name是目录,不确定会发生什么,可能git会读取工作目录)。转换file_name为对象ID(此时将是blob)。如果没有阶段0条目,则报“未合并”消息的错误,除非给定了-m、-ours、-theirs选项。使用-m将“取消合并”文件(删除阶段0条目并重新创建冲突的合并
2),而--ours和--theirs保留任何阶段0条目(已解决的冲突保持解决)。
2. 在任何情况下,如果这还没有出错,那么使用找到的blob SHA-1将存储库副本(或副本,如果file_name命名一个目录)提取到工作目录中。
所以,简单来说就是“是的和不是的”:
git checkout
有时会修改索引,有时只使用它。然而,文件本身从未存储在索引中,只存储在仓库中。如果你
git add
了一个文件,对它进行更改,然后再次
git add
,这会留下一个被git fsck发现的“悬挂blob”:一种没有参考的对象。
1我故意忽略了索引中为使git表现良好并允许--assume-unchanged
等操作而存在的其他内容。(这些与此处的add/checkout操作无关。)
2此重新创建尊重merge.conflictstyle
的任何更改,因此,如果你决定喜欢diff3
输出,并且已经有一个不带diff3
样式的冲突合并,你可以更改git配置并使用git checkout -m
获取具有新样式的新工作目录合并。