我刚刚克隆了一个 GitHub 并且一些(但不是全部)文件被认为已修改,即使 IDE 说它们与存储库版本相同。为什么?

I've just cloned a GitHub and some (but not all) files are considered modified even though IDE says they are identical to repository version. Why?

我刚刚在 https://github.com/aic-sri-international/aic-util 克隆了 GitHub 存储库,一些(但不是全部)文件被 IntelliJ IDEA IDE 和 git status.

存储库中共有 549 个 Java 个文件,其中 49 个被 git status 修改。我没有看到任何明显的模式,其中文件被认为已修改而哪些文件未被修改。

IntelliJ 上的差异 IDEA 表示文件的“内容与存储库的内容相同”。

即使在对文件使用 git restore 之后,它仍然显示为已修改!

A git diff 显示如下:

@@ -1,67 +1,67 @@
-Line 1
-Line 2
...
-Line 67
+Line 1
+Line 2
...
+Line 67 

我想知道这是否是由于 CRLF/LF 差异造成的。我在 macOS 上,原始项目是在 Windows 上开发的。但是,使用 cat -ve 使用 CRLF.

显示已修改和未修改的文件

这是怎么解释的?

编辑;简短的解决方案: 选择的答案非常详细,但要从中找到解决方案需要仔细阅读。

本质上,出于某种原因,存储库正在存储 CRLF 个文件,即使它应该包含 LF 个文件。只需将修改后的文件 git add . --renormalize 添加到 Git 的最新版本(等于或高于 2.16)并提交它们应该会在存储库中创建一个具有正确 LF 格式的新提交。

TL;DR

存储库中的文件是“坏的”(在某种意义上)。如果拥有存储库的人“修复”(在某种意义上)提交的文件,问题就会消失。

理解这个问题的关键在于克隆实际的存储库并访问有问题的提交。存储库(如评论中所述)是 https://github.com/aic-sri-international/aic-util,而有问题的提交(可能是您正在查看的那个)是 60fa84abe4357b4fc0acb3d18cefc5b3c40958b6(我写这篇文章时他们的 master) .

在这次提交中,我们有一个包含以下两行的 .gitattributes 文件:

* text=auto
*.java text eol=crlf

我们也——这很重要——将 blob 对象 存储在存储库中,并带有文字 CRLF 行结尾。例如,对象 97d1f597b409bdd68ad21d98495f85a40e199cbf 是一个 blob 并保存此提交的 src/main/java/com/sri/ai/util/AICUtilConfiguration.java 版本,并且 - 正如 vis1 向我们展示的那样 - 它包含:

$ git cat-file -p 97d1f597b409bdd68ad21d98495f85a40e199cbf | head | vis
/*\^M
 * Copyright (c) 2013, SRI International\^M
 * All rights reserved.\^M
 * Licensed under the The BSD 3-Clause License;\^M
 * you may not use this file except in compliance with the License.\^M
 * You may obtain a copy of the License at:\^M
 * \^M
 * http://opensource.org/licenses/BSD-3-Clause\^M
 * \^M
 * Redistribution and use in source and binary forms, with or without\^M

这是什么意思?

这意味着 提交的文件 包含 CRLF 行结尾。

没有已提交的文件 永远无法更改。提交 60fa84abe4357b4fc0acb3d18cefc5b3c40958b6 包含 此文件 此版本 ,带有 CRLF 行结尾。这将永远是真的。任何地方都无能为力。

好的,但为什么 git status 说它是“修改过的”?

这可能至少稍微取决于你的 Git 年份,但我的确实这么说,正如 OP 所问:

$ 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 restore <file>..." to discard changes in working directory)
        modified:   src/main/java/com/sri/ai/util/AICUtilConfiguration.java
[many more, snipped]

I wondered if this could be due to CRLF/LF differences

是的。

.gitattributes 文件中的 *.java text eol=crlf 行相当于 Git 的两组指令:

  1. 复制此文件时提交到我的工作树(git restore)或 Git对我的工作树的索引(git checkoutgit switch、大多数其他此类命令),将任何仅 LF 行结尾更改为 CRLF 行结尾。

这条指令没有任何问题,但是由于提交中文件中的每一行——以 blob 对象的形式存储——已经有 CRLF 行结尾,所以这条指令不会 do任何东西。

  1. 将此文件从我的工作树复制到 Git 的索引时(以便我对其所做的任何更新现在都准备好提交),将任何 CRLF 行结尾更改为仅 LF 行结尾。

如果我 运行 git add 文件 src/main/java/com/sri/ai/util/AICUtilConfiguration.java,Git 将不会更新索引副本.所以索引副本——目前仍然有 CRLF 行结尾——将进入我所做的任何 new 提交,使 committed 副本的奇怪现象永久化有 CRLF 行结尾。

如果我运行git add,Git可能会也可能不会实际更新文件,取决于 Git 年份和其他细节。这里的问题是 Git 试图变得非常聪明。如果我真正更改了工作树副本——例如,向其添加注释或其他内容——那么 git add 确实必须重新压缩并重新 Git-ify 这个更新的工作树副本,并且将副本更新到 Git 的索引中的内容。这将遵循上面的指令 #2,因此将所有 CRLF 转换为仅 LF。

如果我随后使用该版本的文件提交,该文件将在提交中以其 Git 化的对象形式具有仅 LF 行结尾 .在 49(你数)或 129(我数).java 个文件中,我们将下降一个文件。

我们以后不会看到这个变化(用LF替换CRLF),如果我们git checkoutgit switch-到新的提交,因为我们在工作树中看到的文件遵守规则#1。但是这个文件将不再显示为“已修改”,因为 Git 知道规则 #2 取消了 CRLF-s,将文件变回 Git 索引中的仅 LF 文件.也就是说,我们的工作树、索引和 HEAD 提交都将是和谐的。

在Git的一些版本中,Git这里的“聪明”太聪明了,如果我们碰里面的文件无论如何,git add 不会修复文件。但是,如果我们有 git add --renormalize(Git 2.16 或更高版本),我们可以使用它来修复文件。或者,在 Git 我正在测试的机器上的版本中(Git 2.27.0,有点落后于最新版本,但远远超过 2.16),一个简单的 git add . 就足够了,即使没有 --renormalize.

更多关于 Git 版本依赖性

多年来,Git 中处理行结束转换的代码有点奇怪——也许“脆弱”这个词更合适。 Git 版本 1.7 和 1.8 中出现了各种问题,一些属性在 Git 2.10 中被修复了一段时间,等等。因此,并非 Git 的每个版本都像现代(2.27、2.30 等)Git 那样表现。

还有非 Git 程序(例如 Eclipse 及其内部 JGit libraries)读取和写入 Git 存储库。从 Git 的角度来看,这些不一定“正确”。它们 应该 兼容,但考虑到 Git 本身时不时地得到 CRLF 转换“错误”,如果 JGit 不完全正确,谁会抱怨做与 Git 相同的事情?也许问题不是特定的 Git 版本,而是特定的 something-else 版本。

旁白:如果我讨厌 CRLF 行结尾怎么办?

如果存储库本身添加了修复内部文件的新提交,以便它们具有仅 LF 行结尾,但保留 .gitattributes 指令以在工作树副本中使用 CRLF 行结尾,您将始终在您的工作树、任何新克隆或结帐中看到 CRLF 行结尾。如果您不喜欢这个——例如,如果您在 Mac 或 Linux 上并且不想让 vim 使用其神奇的“文件格式”检测要对您隐藏 CRLF 行结尾,有一个简单的解决方法。您可以在克隆中创建一个 .git/info/attributes 文件。在该文件中,您可以编写 *.java text eol=lf。此行覆盖 .gitattributes 行,因此 Git 将使用两个规则:

  • 结帐时:仅使用 LF,即不要将 CR 添加到 LF(但也不要去除 CR)
  • git add 上:将 CRLF 转换为仅 LF

在新克隆之后必须执行此操作很烦人,但设置起来很容易。请注意,尽管 .git/info/attributes 不是 不是 一个 提交的 文件,并且不在您的工作树中,因此 Git 从不更新它,无论您检出哪个提交。


1vis命令将control-M字符变成\^M,使它们在这里可见;此命令存在于 macOS 和其他基于 BSD 或 BSD 派生的系统上。 vis 能够对文本进行编码以在各种设备之间传输,否则可能会破坏某些编码或使它们不可见(当然,您也可以使用 atob 或 uuencode 来做到这一点,但这些根本不是人类可读的)。

如果你有 cat -v,这也会使 control-M 可见,但不是以完全可逆的方式:包含文字字符序列 ^ M 变得与包含控件-M 的无法区分。尝试,例如,printf 'foo^Mbar\r\n' | cat -v vs printf 'foo\^Mbar\r\n' | vis 看看为什么 vis 更优越:

sh-3.2$ printf 'foo^Mbar\r\n' | cat -v
foo^Mbar^M
sh-3.2$ printf 'foo\^Mbar\r\n' | vis
foo4^Mbar\^M

注意 vis 有很多标志来控制它的输出编码,并不是所有的编码都是完全可逆的——但默认输出 .