首页 > 解决方案 > `git add` 如何处理文件<->目录之类的变化?

问题描述

这是一个很长的问题。我正在尝试对一些基本的 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/subdir导致file1/subdir/something索引文件不一致。

上述案例描绘了一个非常复杂的git add. 我在这里遗漏了什么,还是git add真的不像看起来那么简单?

标签: gitversion-controlreverse-engineeringgit-add

解决方案


实际上,这只是意味着您在(至少某些版本的)Git 中发现了一个错误。

Git 理解操作系统不能支持两个实体,一个是文件,另一个是目录/文件夹,具有相同的名称。也就是说,我们不能file1既是文件 file1目录1

现在,关于 Git 的索引的问题是它根本无法在其中保存目录。2 唯一允许的实体是文件。所以要么file1存在,要么file1/subdir/something存在,但绝不会两者兼而有之。Git 里面有一堆相当复杂的代码,用于索引本身和在git checkoutgit reset等期间处理操作系统级别的文件,这些代码应该处理“D/F”(目录/文件)冲突。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 文件系统支持它。它也会使归档器变得一团糟tar

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


推荐阅读