“git add file” 和 “git checkout -- file” 是否对称?

5
我对git add filegit checkout -- file有以下理解(但我不确定是否正确)。
每当我们使用文本编辑器编辑文件时,都是在工作目录中进行。每次我们可以通过执行git add file_name将文件移动到所谓的“暂存区”。如果我们再次编辑该文件(在git add之后),则会更改工作目录中的文件,并且以这种方式,在工作目录中,我们拥有一个“新”的状态的文件,而在“暂存区”中,该文件处于“旧”状态。
当我们再次使用git add时,我们将“暂存区”中的文件带回到“新”状态(即来自工作目录的状态)。
如果我们执行git checkout -- file_name,我认为我们会从“暂存区”中取出一个文件,并用它覆盖工作目录中的文件。通过这种方式,我们可以将工作目录中的文件带回到“旧”状态。这样说是否正确?
我还不清楚的是,我们是否复制或移动了暂存区中的文件。换句话说,git checkout -- file是否会更改暂存区中文件的状态。我们可以说,在git checkout -- file之后,暂存区中的文件将文件的状态更改为其在暂存区中的上一个状态吗?
2个回答

7

它几乎是对称的,但并非完全对称。

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获取具有新样式的新工作目录合并。


+1. 关于索引的更多信息,请参阅以下链接:https://dev59.com/Cm855IYBdhLWcg3w6IzV#4086986 和 https://dev59.com/0mw15IYBdhLWcg3wSJo3#6718135。 - VonC

0

当你使用git add添加文件时,你标记了你想要提交该文件的确切状态。Git会记住文件的状态并在提交或重置时将其保持不变。因此,暂存后对文件进行的所有操作都是针对工作目录中的文件,而不是暂存区中的文件。
当你运行git checkout时,Git仅会将未暂存的文件更改为HEAD版本。要将已暂存的文件更改为HEAD版本,需要运行git reset


抱歉,我不理解答案,因为我不知道什么是HEAD修订版。 - Roman

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接