问题描述
这是一个很长的问题。我正在尝试对一些基本的 Git 功能进行逆向工程,但在理解 git add
真正在幕后所做的事情时遇到了一些麻烦。我已经熟悉 Git 的三棵树,而且索引文件并不是真正的树,而是树的排序数组表示。
我原来的假设是这样的:当运行git add <pathspec>
时,
- 如果工作目录中存在
<pathspec>
: - 如果
<pathspec>
只存在于当前索引文件中: - 如果工作目录或索引文件中不存在
<pathspec>
:fatal: pathspec <...> did not match any files
这个假设反映了“做你被告知要做的事情”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 somefile
或 git 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 somefile/file
:通常,删除旧somefile/file
的条目。但如果它四处查看,它还应该为somefile
添加新条目。 -
git add somefile
:相当于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 file1
、git add file1/subdir
和 git add file1/subdir/something
。
我的预期:
-
git add file1
:相当于git add .
,导致file1/subdir/something
的单个索引条目。 -
git add file1/subdir
和git add file1/subdir/something
:通常,应该只为file1/subdir/something
添加一个条目(导致索引文件不一致)。但是如果上面的“无不一致索引”理论是正确的,这些也应该删除旧的file1
索引条目,因此相当于git add .
。
实际发生的事情:
-
git add file1
:按预期工作,相当于git add .
。 -
git add file1/subdir
和git add file1/subdir/something
:只为file1/subdir/something
添加单个条目,导致无法提交的不一致索引文件。
我所指的不一致索引文件是:
100644 <object addr> 0 file1
100644 <object addr> 0 file1/subdir/something
因此,仅添加另一层嵌套似乎可以阻止 git add
像在案例 1 中那样四处窥视!请注意,提供给 git add
的路径也无关紧要 - file1/subdir
和 file1/subdir/something
都会导致索引文件不一致。
以上案例描绘了一个非常复杂的 git add
实现。我在这里遗漏了什么,还是 git add
真的不像看起来那么简单?
解决方法
实际上,这只是意味着您在(至少某些版本的)Git 中发现了一个错误。
Git 明白操作系统不能支持两个实体,一个是文件,另一个是同名的目录/文件夹。也就是说,我们不能同时让 file1
作为 file 和 file1
作为 目录。1
现在,Git 索引的问题在于它根本无法在其中保存目录。2 唯一允许的实体是文件。因此,要么 file1
存在,要么 file1/subdir/something
存在,但永远不会同时存在。 Git内部有一堆相当复杂的代码,用于索引本身以及在git checkout
、git reset
等期间处理操作系统级文件,这是假设 处理“D/F”(目录/文件)冲突。 Git 需要在执行 git checkout
的提交时处理这些问题,其中 somefile
是一个文件,然后是不同提交的 git checkout
,其中 somefile/file
是一个文件,所以 {必须删除 {1}} 并插入一个目录。它需要能够处理我们回到第一种情况的结账,因此必须删除somefile
,然后必须将somefile/file
rmdir-ed,然后才能创建somefile/
作为文件。并且,它必须处理合并,其中 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
-as-a-directory 擦除包含 file1
的索引槽:
file1
因为这会触发删除现在不需要的条目的代码。
(很明显,这需要一个修复程序和一个测试套件测试用例。幸运的是,Git 在构建树的过程中会自我检测到错误的情况,因此它不会进行错误的提交。)
1我认为也许我们应该能够做到这一点,但目前POSIX规则禁止这样做,并且没有任何类Unix文件系统支持它。它也会让 $ git add file1
$ git ls-files --stage
100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 file1/subdir/something
之类的归档器变得一团糟。
2严格来说这并不完全正确:出于各种加速目的,索引包含“不规则”(非缓存)条目以及描述提议的下一次提交的正常缓存条目。它是不保存目录存在的缓存条目;未提交的条目可以包含各种辅助信息。但这些都没有被 tar
显示。