即使在 git commit -am b/c origin 有一个文件名大写的文件后,“未准备提交的更改”

“Changes not staged for commit" even after git commit -am b/c origin has a file with de-capitalize filename

问题:同一目录下的两个不同名称大小写下的两个文件,我一开始并不知道。所以我很惊讶地看到这个,

git commit -am "why"
On branch tmp
Changes not staged for commit:
    modified:   src/view/callCenter/seatReport/SeatSubstate.vue

然后我发现origin在路径src/view/callCenter/seatReport中有SeatSubstate.vue & seatSubstate.vue

但是在我的 mac

ls src/view/callCenter/seatReport/
...     seatSubstate.vue /* did NOT show SeatSubstate.vue only seatSubstate.vue */

我知道有人在讨论 How do I commit case-sensitive only filename changes in Git?

但是我还是不明白为什么git不能提交这个文件。

其次,我该如何解决这个问题?例如,在那次 SO 讨论中,许多人回答提到 git mv,但我不确定 git mv 是否可以解决我的问题。

-----更新-----

突然发现我的mac(准确的说是我的HD)不区分大小写(APFS),参考https://apple.stackexchange.com/questions/71357/how-to-check-if-my-hd-is-case-sensitive-or-not

通常它应该意味着 SeatSubstate.vue 和 seatSubstate.vue 是同一个文件,但不知何故 git 使它们成为 2 个不同的文件并引起了麻烦。 git mv 似乎可以解决问题,但我不是 100% 确定。

参考Changing capitalization of filenames in Git

正确定义问题

Git 总是 能够存储——在提交和Git的索引中,也就是说——两个不同名称大小写下的两个文件(例如 READMEreadme)在同一目录中,因为 Git 根本不将文件存储在操作系统目录中.文件要么在提交中被冻结,1 这意味着无论它们是在 Linux 或 Windows 还是 MacOS 或任何其他系统,或者它们在 Git 的索引中,这实际上只是一个数据文件。2

问题的发生是因为您,操作人员 Git,想要使用 OS 提供的文件系统,您的计算机以正常的日常形式存储文件,以便您的其余部分电脑也可以和他们一起工作。这不是一个不合理的要求——Git的内部文件以Git-only的内部形式存储,只有Git可以使用。您需要能够使用 Git 来 完成某事 ,而不仅仅是整天玩 Git。

MacOS 能够提供区分大小写的文件系统(可以将 READMEreadme 保存在同一目录中)但不提供所以默认情况下。所以,要么根本不使用 MacOS,要么使用这个能力,某人——不是你——做了这种事:

Then I found origin has both SeatSubstate.vue & seatSubstate.vue in the path src/view/callCenter/seatReport

换句话说,您在一些现有的提交中有两个文件。正如我们刚才所说,Git 完全有能力处理这个问题。不是你的 OS。

因此,如果您 运行 git checkoutselect 提交,Git 将复制 两者files 到您的索引,它现在有 两种拼写 SeatSubstate.vueseatSubstate.vue。它还将两个文件(两种拼写!)复制到您的工作树,但您的OS只能保存一个拼写,所以一个文件擦除另一个,你只剩下 一个 拼写为 一个 的文件。

当 Git 将索引的文件及其内容与工作树文件及其内容进行比较时,Git 将:

  • 看到,根据索引,有两个文件;
  • 尝试将每个索引文件与工作树文件进行比较 Git 在打开该名称时获得;
  • 抱怨其中一个被修改了

这是一个例子,我通过在 Unix-y 系统上创建一个存储库并给它两个文件,READMEreadme,具有不同的内容,然后将其克隆到 Mac:

sh-3.2$ git clone ssh://[path]/caseissue
...
Receiving objects: 100% (4/4), done.
sh-3.2$ cd caseissue
sh-3.2$ ls
readme

让我们看看索引中的内容:

sh-3.2$ git ls-files --stage
100644 a931371bf02ce4048b623c56beadb9a926138516 0       README
100644 418440c534135db897251cc3ceca362fe83c2117 0       readme

果然有两个文件,只是大小写不同而已。让我们看看 这些文件中,以及工作树中的内容:

sh-3.2$ git show :0:README
I AM AN UPPERCASE FILE
sh-3.2$ git show :0:readme
i am a lowercase file
sh-3.2$ cat readme 
i am a lowercase file

而我们的状态:

sh-3.2$ git status
On branch master
Your branch is up to date with 'origin/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:   README

no changes added to commit (use "git add" and/or "git commit -a")

根据我们需要做的事情,我们可能只知道索引就可以做到,或者我们可能需要直接使用索引,哪个更痛苦


1从技术上讲,冻结文件的内容存储在 blob 对象中,它们的名称存储在 tree 中objects,并且提交是 commit objects,它引用引用 blob 对象的树对象。但是从用户的角度来看,文件被冻结到提交中,所以我们可以在这里使用该措辞。

2索引实际上可以是多个不同的数据文件,您可以将Git指向替代索引文件,并以此做各种花哨的技巧。例如,这就是 git stash 的工作原理。但是 "the" 索引是 Git 构建 您将进行的下一次提交的地方 并且为了我们的目的,这只是文件 .git/index.


如果您不需要这两个文件

,该怎么办

假设您不需要使用 文件。如果您需要以区分大小写的方式处理 both 个文件,这样您就可以对 SeatSubstate.vueseatSubstate.vue 这两个单独文件的内容大惊小怪,显然,您需要设置一个区分大小写的文件系统。但无论您在做什么,我们都可以假定您不需要 文件来完成这项工作。

这里使用的技巧是从 删除 工作树中剩下的一个文件开始,然后忽略 Git 告诉你的事实您有 两个 未准备提交的更改。也就是说,Git 会告诉您两个文件都已删除。

sh-3.2$ rm readme
sh-3.2$ git status
On branch master
Your branch is up to date with 'origin/master'.

Changes not staged for commit:
  (use "git add/rm <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

        deleted:    README
        deleted:    readme

no changes added to commit (use "git add" and/or "git commit -a")

现在,完全不要使用git commit -a,因为那样会导致两次删除。相反,使用剩余的文件(在我的例子中,完全 none),做任何你需要做的事情,然后暂存——git add——仅那些你 修改的文件,没有以任何方式触及 已删除 文件。

您现在可以 git commit 结果而不影响工作树中丢失的两个文件,但仍然存在于您所做的新提交中:

sh-3.2$ echo 'this file is independent of the READMEs' > newfile
sh-3.2$ git add newfile
sh-3.2$ git commit -m 'add new file'
[master 6d5d8fc] add new file
 1 file changed, 1 insertion(+)
 create mode 100644 newfile
sh-3.2$ git push origin master
Counting objects: 3, done.
...
   2dee30f..6d5d8fc  master -> master

在另一台(区分大小写的文件系统)机器上,更新到此提交后:

$ ls
newfile readme  README
$ for i in *; do echo -n ${i}: && cat $i; done
newfile:this file is independent of the READMEs
readme:i am a lowercase file
README:I AM AN UPPERCASE FILE

所以我们完全有能力在我们的Mac(或Windows!)系统上进行这些提交:我们只删除不需要的文件并小心避免暂存删除。

如果您确实需要其中一个文件不需要更改它[=174=,该怎么办]

现在问题有点难了,因为在我们的不区分大小写的工作树中无法保存两个文件两个拼写在我们的 Mac 或 Windows 系统上。

但是我们可以挑选我们得到的文件!假设我们需要 README 文件。我们可以看到我们得到的是上面的 readme 文件。所以我们将删除错误的(好吧,我们已经这样做了),然后:

sh-3.2$ git checkout -- README
sh-3.2$ ls
README  newfile
sh-3.2$ cat README 
I AM AN UPPERCASE FILE

如果我们需要小写字母:

sh-3.2$ rm README 
sh-3.2$ git checkout -- readme
sh-3.2$ ls
newfile readme
sh-3.2$ cat readme
i am a lowercase file

也就是我们去掉错误的一个,然后使用从索引中抓取一个文件操作— git checkout -- <em>path</em>—获取我们do想要的一个文件。我们现在可以使用这个文件。但我们无法添加或更改它。

如果您需要两个 个文件,或者需要处理其中一个文件怎么办?

如果你同时需要这两种命名方式,那么你就有麻烦了,因为你的 OS 从字面上看 不能 做到这一点——至少, 不在这个文件系统上;您需要创建一个区分大小写的文件系统,之后整个问题就消失了。但是,如果您一次只需要一个来进行某种更改,那是我们可以管理的东西,尽管非常笨拙。

首先,请注意,您可以获得一个或两个文件的 内容 非常容易:

sh-3.2$ git show :README
I AM AN UPPERCASE FILE
sh-3.2$ git show :readme
i am a lowercase file

(旁注:字符串 :0:README:READMEgit show 的含义完全相同:从路径名 README 下的索引槽零获取文件。您可以将 git show 的输出重定向到您喜欢的任何文件名,这样您就可以将两个内容放入两个文件中,这些文件的名称是您的 OS 认为 "different" 的。您可以使用 :README:0:README 作为 git show 的参数。对于是否使用 : 前缀形式的索引号,我并不总是一致的。那里的原因 一个:0:的形式就是索引中也有stage 1,2,3槽,只在合并时使用。也就是说,如果索引中有一个:1:README ,这是 README 的合并基础副本;您将在冲突合并期间拥有它。)

正如我们在上面看到的,您还可以删除工作树文件并使用 git checkout -- <path> 将其中 一个 与您选择的案例一起放入您的工作中-具有相同情况的树。遗憾的是,如果您想 修改并重新添加 文件,这并不总是有效:

sh-3.2$ rm readme
sh-3.2$ git checkout -- README
sh-3.2$ echo UPPERCASE IS LIKE SHOUTING >> README
sh-3.2$ git add README 
sh-3.2$ git status
On branch master
Your branch is up to date with 'origin/master'.

Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    modified:   readme

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:   README

哎呀!似乎 Git 已经决定工作树中的 README 文件应该更新索引中的阶段零 readme 文件!果然,这正是 Git 所做的:

sh-3.2$ git show :0:README
I AM AN UPPERCASE FILE
sh-3.2$ git show :0:readme
I AM AN UPPERCASE FILE
UPPERCASE IS LIKE SHOUTING

所以现在我们必须求助于让我们直接写入索引的工具。首先,让我们擦除此更改并返回到我们没有工作树副本的 "clean-ish" 状态。 注意:如果您的实际工作比我的更复杂,您可能希望在 git reset 将其清除之前将其全部保存在其他地方!

sh-3.2$ git reset --hard
HEAD is now at 6d5d8fc add new file
sh-3.2$ rm readme 
sh-3.2$ git status --short
 D README
 D readme

此处的 --short 输出在第二个位置具有 D 字符,表明工作树中缺少这两个文件,但索引副本与 HEAD 复制。所以现在我们可以得到我们想要的文件了,无论是哪个——我会再次选择大写的,因为上次出错了:

sh-3.2$ git checkout -- README
sh-3.2$ cat README 
I AM AN UPPERCASE FILE

现在我们使用普通的计算机工具来处理文件:

sh-3.2$ echo UPPERCASE IS LIKE SHOUTING >> README

当我们需要加回去时,我们必须使用git hash-object -wgit update-index:

sh-3.2$ blob=$(git hash-object -w README)
sh-3.2$ echo $blob
fd109721431e207046a4daefc9712f1424d7f38f

(这里的echo只是为了说明,表示我们得到了一个hash ID)。现在我们需要制作一个格式正确的索引条目,a la git ls-files --stage --full-name。也就是说,我们需要文件的 完整路径 ,相对于树的顶部。因为我的 READMEreadme 文件是 树的顶部,在我的例子中,这只意味着 READMEreadme .对于您的示例,您的两个文件位于 src/view/callCenter/seatReport 中,您需要将其包含在路径名中。

无论如何,将 blob 对象写入 Git 数据库后,我们现在需要更新索引条目:

sh-3.2$ printf '100644 %s 0\tREADME\n' $blob | git update-index --index-info
sh-3.2$ git status --short
M  README
 M readme

这表明我们有一项变更准备提交——提交给 README——还有一项变更未提交,提交给 readme。如果您愿意,这里有更长的 git status

sh-3.2$ git status
On branch master
Your branch is up to date with 'origin/master'.

Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

        modified:   README

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:   readme

更直接的,我们可以使用git show查看索引中的内容:

sh-3.2$ git show :README
I AM AN UPPERCASE FILE
UPPERCASE IS LIKE SHOUTING
sh-3.2$ git show :readme
i am a lowercase file

这就是我们想要的!所以现在我们可以 git commit 结果:

sh-3.2$ git commit -m 'annotate README'
[master ff51464] annotate README
 1 file changed, 1 insertion(+)
sh-3.2$ git push origin master
Counting objects: 3, done.
...
   6d5d8fc..ff51464  master -> master

在类 Unix 系统上:

$ for i in *; do echo -n ${i}: && cat $i; done
newfile:this file is independent of the READMEs
readme:i am a lowercase file
README:I AM AN UPPERCASE FILE
UPPERCASE IS LIKE SHOUTING

您可以随时使用 git hash-object -wgit update-index --index-info

如果您的 OS 无法按照 Git 的索引拼写方式拼写文件或路径名,您仍然可以使用文件的 contents,您 可以 使用任何名称。这样做之后,您可以使用 git hash-object -w 将内容变成冻结的 blob,准备提交,然后使用 git update-index --index-info 将该 blob 哈希写入索引——在所需的暂存槽,通常为零——在 Git 需要的路径名下。

您在此过程中放弃的是明智地使用 git status 的能力,对有问题的文件名使用 git add 以及完全使用 git commit -a 的能力。 Git 需要什么才能使它更方便——尽管它永远不会 100% 方便;为此,你需要你的 OS 来表现——是 重新映射 Git 索引路径到(不同的)本地 OS 路径的能力, 在两个方向上:一个名为 IP 的索引文件,对于某些索引路径 IP,不应假定具有 相同 工作树中的名称,而是其映射名称。映射名称必须唯一映射回索引路径。 (也就是说,映射应该是路径上的双射。)

这不仅适用于大小写折叠问题,也适用于 Unicode 问题:MacOS 以 one 格式存储文件名,并已规范化它们,而 Linux 允许以 each 形式存储文件名。名为 agréable 的文件在 Linux 上可以有两个名称,但在 MacOS.

上只能有一个