几乎是对称的,但不完全是对称的。
git add file 确实将文件复制到舞台(又名“索引”)。但是,它这样做的方式有点奇怪。
在 git repo 中,所有内容都存储为 git“对象”。每个对象都有一个唯一的名称,它的 SHA-1(那些 40 个字符的字符串,例如 753be37fca1ed9b0f9267273b82881f8765d6b23——来自我这里的实际 .gitignore)。该名称是通过计算文件内容的哈希来构造的(或多或少——有一些噱头可以确保您不会从目录树或提交中创建文件,并导致哈希冲突,例如)。 Git 假设无论内容如何,SHA-1 都是唯一的:没有两个不同的文件、树、提交或带注释的标签会哈希到相同的值。
文件(和符号链接)是“blob”类型的对象。因此,git repo 中的文件被散列,并且在某个地方,git 有一个映射:“名为 .gitignore 的文件”到“散列值 753be37fca1ed9b0f9267273b82881f8765d6b23”)。
在 repo 中,目录树存储为“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 这样做(或多或少,我故意掩盖过滤器):
- 计算文件
file_name (git hash-object -t blob) 内容的哈希值。
- 如果该对象不在 repo 中,请将其写入 repo(使用
-w 选项到 hash-object)。
- 更新
.git/index(或$GIT_INDEX_FILE),使其在名称file_name 下映射到来自git hash-object 的名称。这始终是“阶段 0”条目(这是正常的、无合并冲突的版本)。
因此,文件并不是真正“在”暂存区,而是真正“在”存储库本身!暂存区域中的内容是名称到 SHA-1 映射。
相比之下,git checkout [<tree-ish>] -- file_name 这样做:
-
如果给定 <tree-ish>(提交名称、树对象 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 条目(已解决的冲突保持已解决)。
-
在任何情况下,如果这还没有出错,请使用由此定位的 blob SHA-1(s) 将 repo 副本(或副本,如果 file_name 命名一个目录)提取到工作目录中。
所以,简短的版本是“是和否”:git checkout 有时会修改索引,有时只使用它。但是,文件本身永远不会存储在索引中,只会存储在 repo 中。如果您 git add 一个文件,再对其进行更改,然后再次 git add 它,这会留下 git fsck 将发现的“悬空 blob”:一个没有引用的对象。
1我故意省略了索引中的许多其他内容,以使 git 表现良好,并允许 --assume-unchanged 等。(这些与添加/结帐无关在这里行动。)
2此重新创建尊重对merge.conflictstyle 的任何更改,因此如果您决定喜欢diff3 输出并且在没有diff3 样式的情况下已经有冲突合并,您可以更改git config 并使用git checkout -m 获得一个新的工作目录与新样式合并。