为什么Git合并索引文件而不是在签出提交时完全覆盖它

时间:2017-08-23 14:15:32

标签: git

这是关于Git内部的问题。我在这里使用低级命令而不使用分支。

设置

echo f1 > f1.txt
echo f1 > f1.txt
git add .
git commit -m "first"
...cf178d5

现在我想使用索引和f3.txt命令创建一个包含一个文件write-tree的新提交:

$ rm f1.txt f2.txt
$ echo ‘f3 content’ > f3.txt
$ git add .

所以目前索引文件和目录只包含新的f3.txt文件:

$ git ls-files -s
100644 [some hash] 0       f3.txt

$ ls
f3.txt

这导致后来的奇怪行为

所以我将树写入存储库并使用新的提交哈希更新HEAD:

LATEST_TREE_HASH=$( git write-tree )
echo $LATEST_TREE_HASH > .git/HEAD

如果我现在运行git status我得到:

$ git status
Not currently on any branch.
nothing to commit, working directory clean

问题

如果我现在使用两个文件firstf1.txt签出f2.txt提交:

$ git checkout cf178d5
A       f3.txt                    <--------------- why?
HEAD is now at a27a75a... initial commit

Git工作正常,但我相信它会合并索引中的树而不是覆盖。您可以从git checkout输出中看到它将f3.txt视为添加的文件,如果我检查索引文件内容:

$ git ls-files -s
100644 [some hash] 0       f1.txt
100644 [some hash] 0       f2.txt
100644 [some hash] 0       f3.txt

$ ls 
f1.txt f2.txt f3.txt

它显示三个文件。有人可以解释一下这种行为吗?

PS。如果您想要提问,因为它与日常的git工作流程无关 - 请不要。

1 个答案:

答案 0 :(得分:2)

修改:问题已经改变,足以使之前的回复无效。

还有一个错字(f1.txt列出两次)和时髦的非ASCII Unicode引号,但我们现在可以看到这里出了什么问题:

$ LATEST_TREE_HASH=$( git write-tree )
$ echo $LATEST_TREE_HASH > .git/HEAD

这有点问题。正如Mark Adelsberger noted in a comment和您的脚本在此处使用TREE一词所述,git write-tree会写一个,而不是提交。

为什么这是一个问题

.git/HEAD中的内容应该是两件事之一:

  • ref: refs/heads/name形式的字符串,其中 name 是有效的分支名称,或
  • 提交对象的哈希ID

反过来,分支名称 - refs/heads/name - 形式的引用必须始终指向提交对象,而不是指向blob,树或标记对象。

这意味着一般的Git假设无论.git/HEAD出现什么,它都指的是提交对象。通过将此树形哈希写入.git/HEAD,您违反了此假设。但是,为了允许未出现的分支机构&#34;,例如尚未master的初始存储库的状态,HEAD可以包含名称实际上并不存在的分支。

接下来发生的事情,我认为,不能保证。 git checkout命令假定如果HEAD包含有效散列,则它包含提交散列,唯一允许的另一种可能性是HEAD包含孤立分支的名称。因此,我们运行git checkout target_hash,如您的示例所示:

git checkout cf178d5

案例1:从提交转为提交

假设HEAD包含有效的提交哈希。让我们将其称为哈希,区别于目标提交哈希。在这种情况下,git checkout会:

  • old 树的内容(根据子树需要递归)与 target 树的内容进行比较。 1
  • 对于必须更改的每个哈希,包括添加或删除,请检查当前索引和工作树中的索引和/或工作树文件版本是否与中的版本匹配。
  • 如果全部匹配,则更新索引哈希并将新文件复制到工作树(或从工作树中删除文件并删除索引条目,如果适用)。
  • 否则(某些文件不匹配):抱怨并拒绝结帐。

显然--force会禁用该检查,但this is the basic process by which both staged and unstaged modifications are carried from one checkout to another when switching branches without being in a "clean" state。该过程在Two Tree Merge section of the git read-tree documentation中的所有血腥细节中进行了描述。

案例2:从孤儿分支转为提交

规则允许的另一种可能性是您目前在孤儿分支上。在这种情况下,没有当前提交。最有可能的是,Git只使用the empty tree ,就像它是当前的提交一样。然后它遵循案例1的相同规则,现在允许它,因为它有一棵树。

但显然,这不是保证。如果Git要使用存储在.git/HEAD中的当前(有效)作为基本树而不是空树,然后按照案例1进行操作,那么您将看到你的两个文件被删除。请关注git read-tree中列出的所有子案例,其中$H设置为现有树,而$H设置为空树。 (我承认没有这样做,但我认为这是行为的来源。但请参阅阅读树文档中关于案例3的评论!)

1 Git实际上是使用存储在index.lock文件中的临时索引来实现的。如果一切顺利,临时索引将重命名为常规索引,在此过程中解锁索引。如果情况不妙,Git会删除临时index.lock文件,丢弃临时索引并解锁索引。

原始答案(有些不同的问题)

还有另一组时髦的非ASCII引号,它们使你的指令剪切和粘贴失败,所以当我做了正常的第一次提交时,我最终得到了两个文件:

$ git commit -m "second commit"
[master (root-commit) b9c7e4b] second commit
 2 files changed, 1 insertion(+)
 create mode 100644 f3.txt
 create mode 100644 f3.txtcontentecho
$ ls
f3.txt                  f3.txtcontentecho
$ git ls-files -s
100644 5927d85c2470d49403f56ce27afd8f74b1a42589 0       f3.txt
100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0       f3.txtcontentecho

但请注意,我的ls vs ls-files -s输出与您的输出大不相同:

  

所以目前索引文件和目录只包含新的f3.txt文件:

$ git ls-files -s
100644 [some hash] 0       f3.txt

$ ls
f1.txt f2.txt

我一点都不清楚你为什么现在在你的工作树中有文件f1.txtf2.txt;我没有。

现在我们使用git commit-tree创建一个提交并运行git checkout

$ INITIAL_COMMIT_HASH=$( \
>     echo 'initial commit' | git commit-tree $INITIAL_TREE_HASH )
$ git checkout $INITIAL_COMMIT_HASH

但我得到的是非常不同的:

Note: checking out 'cd1bc16160c8a2814cd94bc8397230ffe5a16c22'.

You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by performing another checkout.

If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -b with the checkout command again. Example:

  git checkout -b <new-branch-name>

HEAD is now at cd1bc16... initial commit

并且所有内容都按预期读取(文件f1.txtf2.txt位于工作树和索引中; f3文件都不可见。运行git log --graph --all显示预期的两个(断开连接)提交(两者都是root提交,没有父项)。