`git add` 如何处理文件<->目录等变化?
How does `git add` deal with changes like file<->directory?
这是一个很长的问题。我正在尝试对一些基本的 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 .
)
我的预期:
git add somefile
:相当于git add .
- 删除旧条目并添加新条目
git add somefile/file
:只为新的somefile/file
添加一个索引条目。
实际发生的情况: 上述命令中的任何一个都直接导致 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 somefile/file
:导致一个空的索引文件——所以,它做了我通常期望它做的事情!
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
添加一个条目(导致索引文件不一致)。但是如果上面的“no-inconsistent-index”理论是正确的,这些也应该删除旧的 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 理解 OSes 不能支持两个实体,一个是文件,另一个是 directory/folder,具有相同的名称。也就是说,我们不能让 file1
既是 文件 又 file1
是 目录.1
现在,关于 Git 索引的问题是它根本无法在其中保存目录。2 唯一允许的实体是文件。所以要么 file1
存在,要么 file1/subdir/something
存在,但绝不会同时存在。 Git 里面有一堆相当复杂的代码,用于索引本身和在 git checkout
、git 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
。
这是一个很长的问题。我正在尝试对一些基本的 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 .
)
我的预期:
git add somefile
:相当于git add .
- 删除旧条目并添加新条目git add somefile/file
:只为新的somefile/file
添加一个索引条目。
实际发生的情况: 上述命令中的任何一个都直接导致 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 somefile/file
:导致一个空的索引文件——所以,它做了我通常期望它做的事情!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
添加一个条目(导致索引文件不一致)。但是如果上面的“no-inconsistent-index”理论是正确的,这些也应该删除旧的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 理解 OSes 不能支持两个实体,一个是文件,另一个是 directory/folder,具有相同的名称。也就是说,我们不能让 file1
既是 文件 又 file1
是 目录.1
现在,关于 Git 索引的问题是它根本无法在其中保存目录。2 唯一允许的实体是文件。所以要么 file1
存在,要么 file1/subdir/something
存在,但绝不会同时存在。 Git 里面有一堆相当复杂的代码,用于索引本身和在 git checkout
、git 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
。