`git add` 如何处理文件<->目录等变化?

How does `git add` deal with changes like file<->directory?

这是一个很长的问题。我正在尝试对一些基本的 Git 功能进行逆向工程,但在理解 git add 的真正作用时遇到了一些麻烦。我已经熟悉 Git 的三棵树,并且索引文件并不是真正的树,而是树的排序数组表示形式。

我原来的假设是这样的:当git add <pathspec>是运行时,

这个假设反映了“做你被告知要做的事”git add,它只查看路径并记录这条路径上或下面的变化索引文件。对于大多数情况,这就是实际 git add 的工作方式。

但有些情况看起来不是很简单:

1。用目录替换文件

git init

touch somefile
git add . && git commit

rm somefile
mkdir somefile && touch somefile/file

此时,索引文件仅包含我刚刚删除的 somefile 文件的一个条目,正如预期的那样。现在我执行 git add。我有两种方法可以做到这一点:git add somefilegit add somefile/file。 (显然我在这里排除了琐碎的 git add .

我的预期:

实际发生的情况: 上述命令中的任何一个都直接导致 somefile/file 具有单个索引条目的最终状态 - 即,两者都等同于 git add ..

在这里,感觉 git add 不是你直截了当的“让你做什么”的命令。 git add somefile/file 似乎在提供的路径内部和周围偷看,意识到 somefile 不再存在并自动删除索引条目。

2。用文件替换目录

git init

mkdir somefile && touch somefile/file
git add . && git commit

rm -r somefile && touch somefile

此时,索引文件按预期包含旧 somefile/file 的单个条目。同样,我在相同的两个变体中执行 git add

我的预期:

实际发生了什么:

在这里,git add 的行为就像一个“让你做什么”的命令。它只选择路径并用工作目录反映的内容覆盖索引文件的适当部分。 git add somefile/file 不会四处寻找,因此不会自动为 somefile 添加索引条目。

3。索引文件不一致

到目前为止,一个可能的理论可能是 git add 试图避免索引文件不一致的情况 - 即索引文件不代表有效的工作树。但是多一层嵌套就可以做到这一点。

git init

touch file1
git add . && git commit

rm file1 && mkdir file1 && mkdir file1/subdir
touch file1/subdir/something
git add file1/subdir/something

这和情况1类似,只是这里的目录多了一层嵌套。此时,索引文件仅包含预期的旧 file1 条目。同样,现在我们 运行 git add 但具有三个变体:git add file1git add file1/subdirgit add file1/subdir/something.

我的预期:

实际发生了什么:

我指的不一致索引文件是:

100644 <object addr> 0  file1
100644 <object addr> 0  file1/subdir/something

因此,仅添加另一层嵌套似乎可以阻止 git add 像案例 1 中那样四处张望!请注意,提供给 git add 的路径也无关紧要 - file1/subdirfile1/subdir/something 都会导致索引文件不一致。

以上案例描绘了 git add 的一个非常复杂的实现。我是不是漏掉了什么,或者 git add 真的不像看起来那么简单?

实际上,这只是意味着您在 Git.

(至少某些版本)中发现了一个错误

Git 理解 OSes 不能支持两个实体,一个是文件,另一个是 directory/folder,具有相同的名称。也就是说,我们不能让 file1 既是 文件 file1 目录.1

现在,关于 Git 索引的问题是它根本无法在其中保存目录。2 唯一允许的实体是文件。所以要么 file1 存在,要么 file1/subdir/something 存在,但绝不会同时存在。 Git 里面有一堆相当复杂的代码,用于索引本身和在 git checkoutgit reset 期间处理 OS 级别的文件,等等, 应该 处理“D/F”(directory/file)冲突。 Git 在执行 git checkout 提交时需要能够处理这些,其中 somefile 是一个文件,然后 git checkout 另一个提交 somefile/file 是一个文件,因此必须删除 somefile 并且必须插入一个目录。它需要能够处理我们回到第一种情况的结帐,因此 somefile/file 必须被删除,然后 somefile/ 必须被 rmdir-ed,然后 somefile 可以被创建作为一个文件。而且,它必须处理合并,其中 somefile 是三个提交中的一个或两个提交中的文件,但 somefile/file 存在于其他两个或一个提交中。

显然,有人遗漏了一个角落案例。我能够使用您的步骤自己重现此内容,并且:

$ git ls-files --stage
100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0       file1
100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0       file1/subdir/something
$ git write-tree
You have both file1 and file1/subdir/something
fatal: git-write-tree: error building trees

这种状态不应该存在。添加 file1-as-a-directory 擦除 包含 file1:

的索引槽
$ git add file1
$ git ls-files --stage
100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0       file1/subdir/something

因为这会触发删除现在不需要的条目的代码。

(很明显,这需要一个修复程序和一个测试套件测试用例。幸运的是 Git 在树构建过程中自我检测错误的情况,因此它不会做出错误的提交。 )


1我想也许我们应该能够做到这一点,但目前 POSIX 规则禁止这样做类 Unix 文件系统的 none 支持它。它也会让像 tar 这样的存档器变得一团糟。

2这并不完全正确:出于各种加速目的,索引包含“不规则”(非缓存)条目以及描述建议下一次提交。它是不保存目录存在的缓存条目;不是待提交的条目可以包含各种辅助信息。但其中 none 显示为 git ls-files