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

时间:2021-03-11 08:06:28

标签: git diff

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

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

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

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

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 格式创建一个新提交。

1 个答案:

答案 0 :(得分:3)

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]
<块引用>

我想知道这是否是由于 CRLF/LF 差异造成的

是的。

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

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

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

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

如果我对文件 git add 运行 src/main/java/com/sri/ai/util/AICUtilConfiguration.java,Git 将不会更新索引副本。因此,索引副本(目前仍然具有 CRLF 行结尾)将进入我所做的任何 提交,从而使 已提交 副本具有 CRLF 行结尾的奇怪现象永久存在。< /p>

如果我确实运行 git add,Git 可能会或可能不会实际更新文件,具体取决于 Git 年份和其他详细信息。这里的问题是 Git 试图变得非常聪明。如果我真的改变了工作树副本——例如,向它添加评论或其他东西——那么 git add 真的必须重新压缩和重新 Git-ify 这个更新的工作树副本,以及更新副本的东西进入 Git 的索引。这将遵守上面的第 2 条指令,因此将所有 CRLF 转换为仅 LF。

如果我然后使用此版本的文件提交,则该文件将在提交中具有仅 LF 的行结尾,采用 Git 化的对象形式。在 49 个(您的计数)或 129 个(我的计数).java 个文件中,我们将减少一个文件。

如果我们git checkoutgit switch-到新的提交,我们将不会看到此更改(用LF 替换CRLF),因为我们< em>see,在我们的工作树中,遵守规则 #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 . 就足够了,即使没有 {{ 1}}。

更多关于 Git 版本依赖

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

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

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

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

  • 签出时:仅使用 LF,即不要将 CR 添加到 LF(但也不要去除 CR)
  • .gitattributes 上:将 CRLF 转换为仅 LF

在新克隆之后必须这样做很烦人,但设置起来很容易。请注意,git add 不是不是一个提交的文件,并且不在您的工作树中,因此无论您签出哪个提交,Git 都不会更新它。


1.git/info/attributes 命令将 control-M 字符转换为 vis 以使其在此处可见;此命令存在于 macOS 和其他基于 BSD 或 BSD 派生的系统上。 \^M 能够对文本进行编码,以便在各种设备之间传输,否则这些设备可能会破坏某些编码或使它们不可见(您当然也可以使用 atob 或 uuencode 执行此操作,但这些根本不是人类可读的)。

如果您有 vis,它也会使 control-M 可见,但不是以完全可逆的方式:包含文字字符序列 ^ M 变得与包含控制-M 的一个没有区别。尝试,例如,cat -vprintf 'foo^Mbar\r\n' | cat -v 以了解为什么 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 foo\134^Mbar\^M 有许多标志来控制其输出编码,并非所有编码都是完全可逆的——但默认输出