如果你运行:
git rm -r --cached src
git add src
您应该一切顺利(通过git status 确认)。您无需向.gitignore 添加任何内容。你也可能实际上不需要git add 任何东西。
这个答案的问题在于它只是一个食谱;它不会告诉您下次遇到各种问题时该怎么做。
长
首先,让我们正确地定义问题。我们需要注意以下几点:
-
Git 存储提交,而不是文件。也就是说,当您运行 git checkout 或 git commit 时,您处理的存储单元是 提交。确实提交 contain 文件,但这是一揽子交易。 (有一些有点笨拙的零碎工作方法,我们稍后会介绍;它们对特殊情况很有用。)
-
任何提交的所有部分都是只读的。这包括所有存储的文件。它们以永久冻结的压缩格式存储。每个文件的内容也会针对所有其他存储的文件进行重复数据删除。这一点很重要,因为每个提交都存储每个文件的完整副本,但是通过重复数据删除,这意味着大多数提交大多与其他每个提交共享几乎所有文件。文件是只读的这一事实使这一点成为可能:更改一个存储的文件实际上是不可能的,所以如果提交 X 的文件 F1 需要与提交 Y 的文件 F2 相同的内容,这是完全合理的他们实际使用相同的存储内容。
-
您看到和处理/使用的文件不在 Git 中。在 Git 中的文件不是您看到并使用/使用的那些。这是前一项的直接结果。在任何给定的提交中,存储的文件都采用只有 Git 可以读取的形式,并且实际上没有任何东西——甚至 Git 本身——可以部分或部分覆盖它们完全地。这意味着 in Git 中的文件对于完成任何实际工作完全没有用处。所以 Git 不会尝试这样做。当您选择要处理/使用的提交时,Git 将提交的文件out复制到工作区,我们称之为您的工作树 em> 或 工作树。
现在我们了解到您可以看到和使用的文件在 Git 中并不,而在 Git 中在的文件在一个特殊的 Git-唯一的形式,现在我们可以理解这里发生了什么。当 Git 存储文件时,这些文件不在文件夹中并且可以有几乎任意的名称。在 Git 提交中,我们没有名为 src 或 Src 的 文件夹 存储名为 somefile.txt 的 文件。相反,我们只有一个名为src/somefile.txt 的文件,其中包含一个斜线。1 而且——这是我们这里问题的关键——这些名称始终区分大小写,因此提交可以轻松地同时包含src/somefile.txt 和 Src/somefile.txt,作为两个单独的文件。一个提交也可以同时包含readme.md 和README.md,等等。
同样,这些不是普通文件。它们以一种特殊的、只读的、仅限 Git 的、压缩的和去重复的格式存储,只有 Git 可以读取(除了一个初始创建来进行新的提交之外,没有任何东西可以写入)。而且,它们区分大小写,并且可以拼出您在 Windows 机器上根本无法使用的文件名。2
如果您使用的是典型的 Mac 或 Windows 系统,您的常规文件会保留大小写但不区分大小写。这实际上是特定于文件系统的,并且很容易设置一个 macOS 可挂载磁盘,其中文件区分大小写,以便您可以存储readme.md和README.md。有关在 macOS 上执行此操作的方法,请参阅 my answer here。在 Windows 上,您可以设置 VM 并运行 Linux;我不使用 Windows,所以我没有固定程序。
1从技术上讲,已提交版本的文件在内部使用类似文件夹的结构;将文件夹展平的地方是 Git 的 index。但是无论如何你都不能直接处理提交:你将现有的提交读入 Git 的索引,并在 Git 的索引中建立新的提交,这里的文件名实际上包括斜杠。所以我们不妨直接说文件名中有斜线。
2Mac 用户可以嘲笑无法创建名为 aux.txt 的文件的 Windows 用户,因为在 Mac 上创建 aux.txt 很容易。但是 Mac 用户还有其他问题:例如,文件名agréable 的拼写有两种方法,并且只有一种方法可以在 Mac 上使用。虽然这有时是件好事——我们可能希望有两个不同的文件似乎具有相同的名称——但它带来了完全相同的互操作性问题。
在 macOS 上设置问题案例
由于我手边有一台 Mac,我可以说明我们如何设置问题案例。我们从一个新的、完全空的存储库开始:
sh-3.2$ cd ~/tmp && mkdir case && cd case && git init
Initialized empty Git repository in /Users/torek/tmp/case/.git/
sh-3.2$ mkdir Dir && touch Dir/file && giit add Dir/file && git commit -m initial
[master (root-commit) 07ed87d] initial
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 Dir/file
sh-3.2$ mv Dir dir
sh-3.2$ echo some contents > dir/file
现在,到目前为止,我们在 Git 本身 中所做的一切都是创建一个初始的空提交,其中包含一个名为 Dir/file 的文件。然而,最后两个命令告诉 macOS 修改我们的工作区——我们的工作树——将Dir(初始大写字母)重命名为dir(小写字母),并将一些内容放入给定的文件。运行ls 表明操作系统确实重命名了工作树目录,但Git 知道在这个文件系统上,dir/file 和Dir/file 都可以读取或写入相同的普通文件。所以:
sh-3.2$ ls
dir
sh-3.2$ cat dir/file
some contents
sh-3.2$ cat Dir/file
some contents
您可以看到我们所做的更新。但是:
sh-3.2$ git status
On branch master
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: Dir/file
no changes added to commit (use "git add" and/or "git commit -a")
Git 知道dir/file 和Dir/file 都可以访问同一个文件。此外,Git 知道我们的 last 提交中有一个空的 Dir/file。 Git 假设,因此,我们希望保留文件名的先前拼写:Dir/file。 (请注意,名称中包含斜线:没有文件夹,只有文件。)
深入了解:Git 的索引和core.ignorecase
这里涉及多个相互交织的部分:
-
首先,当我们签出某个提交时,Git 会构建一个 Git 调用的数据结构,例如 index 或 暂存区 em>,或者——现在很少见——缓存。这三个名字反映了这件事的重要性,或者说“索引”不是一个很好的名字,或者两者兼而有之。
我喜欢将 Git 的索引总结为 您提议的下一次提交。这就是它作为暂存区的作用。但它并不完全完整,我们稍后会看到。尽管如此,这里的关键思想是,当您第一次检查某个特定的提交时,Git 复制所有提交的文件到它的索引。2 index - 建议的 next 提交 - 现在与 current 提交匹配。如果并且当您进行更改时,您必须让 Git 将更新的文件复制到索引中,这就是 git add 的用武之地。
请记住,索引包含文件的全名,并带有正斜杠。这些都保存在数据文件中——目前是.git/index,可能还有.git 目录中的一些附加文件——这些文件不是由您的操作系统管理,而是由Git 管理。所以它们取决于 Git,Git 说这些文件名是区分大小写的,不管发生了什么。因此,索引可以同时存储readme.md 和 README.md,或同时存储dir/file 和 Dir/file。
-
同时,您的 work-tree 或 working tree 包含您的所有文件,扩展为可用形式。但是,如果您的操作系统将它们视为区分大小写,您将无法在此处同时存储readme.md 和 README.md。您将无法同时存储dir/file 和 Dir/file。您的操作系统将dir 和Dir 视为单个文件夹名称,将readme.md 和README.md 视为单个文件名。因此,Git 的提交可以在此处包含两个名称,但 您的操作系统的文件系统不能这一事实导致了我们的问题。
-
最后,当 Git 设置 Git 存储库时(例如,在我们上面的示例中为 git init 时间),Git 会探测您的文件系统以查看它是否忽略大小写。如果您的操作系统确实忽略大小写,从而可能发生此问题,Git 将 core.ignorecase 设置为 true:
sh-3.2$ git config --get core.ignorecase
true
Git 使用它来“知道”Dir/file 和 dir/file,虽然与 Git 不同,但与 您的主机操作系统的文件系统 相同。
2索引中的实际内容是:
- 文件名,包含正斜杠;
- 文件的模式,通常是
100644(读/写)或100755(读/写但也可执行);
- 一个 blob 哈希 ID,其作用类似于文件内容的链接,已去重并采用内部 Git 格式;和
- 标志和其他内部缓存数据可帮助 Git 跟踪您的工作树中的内容。
由于内容实际上位于其他位置,因此索引不包含文件的真正副本。但是,副本管理是全自动的。这意味着索引副本就像文件的副本一样,尽可能不占用任何磁盘空间(如果内容与某些现有的已提交文件重复)。
所有这一切的最终结果是每个文件的索引“副本”已准备好进入 next 提交。换句话说,索引中的内容就是上演的内容。但是,如果您有一万个文件,当其中 9997 个文件与当前提交中已经提交的文件相同时,将它们全部列出会很痛苦和无聊。所以git status 不会告诉你 9997 identical 文件。它只是说 不 匹配的三个是“为提交而准备的”。其实一万个都是上演的;我们只是保持git status 可用,不说任何关于匹配的内容。
Git 如何使用core.ignorecase
所以,在我们上面的设置中,我们:
-
在 Mac 或 Windows 不区分大小写的系统上创建了一个新的空存储库,其中我们不能同时拥有 README.md 和 readme.md 文件,我们不能同时拥有 @ 987654384@ 和 dir/file。如果我们有一个名为Dir(大写)的目录并且我们尝试创建dir/another,系统将创建Dir/another。
这不是 Git 的问题,但 Git 确实必须处理它。
-
创建并提交了一个空的Dir/file。这现在在一个提交中。它永远无法改变!该提交,只要它存在,就在其中包含该路径名,并带有特定的大小写。
-
使用了一些操作系统端工具(在 macOS 上是纯 mv,我不确定你会在 Windows 上使用什么)将 Dir 重命名为 dir。
由于主机文件系统不区分大小写,Git 在我们创建存储库时将core.ignorecase 设置为true。我们现在可以对 Git 撒谎,并说我们的操作系统将这些情况视为不同。这不是真的!但我们可以实际上是故意暂时对 Git 撒谎。如果出现问题,我们将负责:除非您愿意承担责任,否则请勿这样做。
sh-3.2$ echo some contents > dir/file
sh-3.2$ git status --short
M Dir/file
sh-3.2$ git config core.ignorecase false
sh-3.2$ git status --short -uall
M Dir/file
?? dir/file
(第一行是重复的,但因为我使用了>,所以我可以随意重复它——它会擦除文件并在其中添加一行读取some contents)。
在这里,我使用git status --short 来缩短输出。 M 表示工作树副本已被修改。在对 Git 撒谎之前,Git 检查并发现 dir/file 存在,找出这与索引 Dir/file 匹配,并将它们匹配起来。但是,一旦我对 Git 撒谎,就会发生一些有趣的事情:
-
Git 相信有一个 Dir/file。它会尝试打开该文件,假设它不会得到dir/file。但它确实得到dir/file。它将dir/file 与它认为得到的Dir/file 进行比较,发现它有所不同。所以现在 Git 说该文件已修改(在工作树中状态为 M)。
-
Git 发现名为dir 的目录/文件夹,读取它并找到一个名为file 的文件。 dir/file 的路径在 Git 的索引中 not,所以 Git 说这个文件是 untracked(这是双问号)。
我现在可以git add dir/file:
sh-3.2$ git add dir/file
sh-3.2$ git status --short
M Dir/file
A dir/file
Git 使用小写名称将dir/file 复制到其索引中。 此文件副本的内容为some contents。 git status 输出现在将其显示为 new 文件。那是因为当前提交没有dir/file;它只有一个Dir/file(内容不同)。
我现在可以进行新的提交了。这个新的提交包含Dir/file(仍然是空的)和dir/file,内容为some contents:
sh-3.2$ git commit -m 'add lowercase version with content'
[master 793d32c] add lowercase version with content
1 file changed, 1 insertion(+)
create mode 100644 dir/file
sh-3.2$ git show HEAD:Dir/file
sh-3.2$ git show HEAD:dir/file
some contents
sh-3.2$ git ls-tree -r HEAD
100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 Dir/file
100644 blob f70d6b139823ab30278db23bb547c61e0d4444fb dir/file
这里的blob 行是Git 表示有两个文件的方式。这两个文件的模式是100644(读/写但不执行),大而丑陋的哈希ID是Git对内容进行重复数据删除的方式;文件的名称出现在右侧。 -r 到 git ls-tree 的选项需要让它显示每个“文件夹”中的内容(另请参见脚注 1:如果不是因为索引扁平化文件夹,Git 会 em> 能够存储一个空文件夹,但是因为索引做了它的事情,Git 不能)。
现在我已经提交了this,这个冻结状态——有两种不同的名称——永远存在,或者至少,只要第二次提交继续存在。即使在不区分大小写的 Mac 文件系统上,我也能够通过对 Git 撒谎的伎俩做到这一点。
我现在可以在恢复 core.ignorecase 之前做最后一招:
sh-3.2$ git rm --cached Dir/file
rm 'Dir/file'
sh-3.2$ git status --short
D Dir/file
sh-3.2$ ls
dir
sh-3.2$ ls dir
file
实际上,我本来可以恢复 core.ignorecase,因为 git rm --cached 不需要花哨的大小写技巧:它总是直接从 Git 的索引中删除条目,而且这些条目总是区分大小写的。但我想我会像这样在这里展示它。让我们把core.ignorecase 放回原来的样子,然后提交结果:
sh-3.2$ git config core.ignorecase true
sh-3.2$ git commit -m 'remove uppercase Dir/file'
[master c5edc17] remove uppercase Dir/file
1 file changed, 0 insertions(+), 0 deletions(-)
delete mode 100644 Dir/file
sh-3.2$ git status
On branch master
nothing to commit, working tree clean
我已经用我想要的内容进行了更正的提交,现在我有一个可以在这台 Mac 上正确使用的提交,即使在不区分大小写的文件系统中也是如此。中间提交,其中包含Dir/file 和 dir/file,无法在典型的 Windows 或 Mac 设置上正确使用——但因为 Git 确实有效在进行新提交时,使用 index,我们可以通过谨慎的技巧来使用这样的存储库。这不有趣,更好的工具可能会更好,但这表明你可以如何克服一些问题。
请注意,当您关闭 core.ignorecase 时,您 有责任确保文件名大小写正确。 Git 会错误地认为创建dir/file 将创建dir/file,即使存在名为Dir/ 的文件夹。您必须手动 rm -r 整个 Dir 目录,或者将其重命名:
mv Dir save
git checkout -- dir/file
例如,当 Git 去创建 dir 时,操作系统不会只是愉快地使用现有的 Dir/ 目录。
一旦你完成了这种手动、小心、一个文件的操作,重新打开core.ignorecase 绝对是个好主意(假设它最初是打开的:使用git config --get 找出答案) -时间挖掘。
注意:
git checkout <commit> -- <path>
告诉 Git 从一些特定的提交中提取给定的<path>:
- 将其复制到 Git 的索引中:关闭
core.ignorecase 后,Git 会认为可以使用不同大小写的相似名称;
- 将生成的索引文件复制到您的工作树中;这是您有责任确保任何现有文件夹的大小写正确的地方。
这就是我上面所说的“零碎”的意思,在长部分的顶部。形式:
git checkout -- <path>
告诉 Git 从 Git 索引中的当前内容中提取给定路径。
我们在一种情况下使用提交说明符而在另一种情况下省略它,这一事实令人困惑。如果您有 Git 2.23 或更高版本,则可以使用 git restore 而不是 git checkout。 restore 命令允许您将某些内容的来源指定为提交或索引。如果您选择提交,您可以指定是将文件复制到到索引,还是您的工作树,或两者兼而有之。如果您选择索引作为来源,您可以将文件复制到到的唯一位置就是您的工作树。
(最后一个事实是,您的工作树和 Git 的索引都可以写入,但提交只能读取。)