git 显然一直说文件已被修改,但实际上尚未修改

Dan*_*ini 2 git repository line-endings

我遇到了一个奇怪的情况,\n可能与这个问题有关,\n但我想更好地了解这里发生的情况。

\n

我有一个存储库,在克隆后立即git status报告文件已被修改。

\n

我在这里创建了一个最小的复制品,\n其中包含一个仅包含忽略列表的存储库,这.gitattributes是一个非常琐碎的文件,\n以及导致我头疼的文件:gradlew.bat

\n

我在下面的所有尝试都是使用 Linux/ZSH 执行的(该问题已在多个 Linux 安装和 shell 上重现)。

\n

在克隆之后,如果我运行git status,我会得到:

\n
\xe2\x9d\xaf git status\nOn branch master\nYour branch is up to date with \'origin/master\'.\n\nChanges not staged for commit:\n  (use "git add <file>..." to update what will be committed)\n  (use "git restore <file>..." to discard changes in working directory)\n        modified:   gradlew.bat\n\nno changes added to commit (use "git add" and/or "git commit -a")\n
Run Code Online (Sandbox Code Playgroud)\n

如果我尝试使用 查看未修改的版本,则再次git checkout HEAD -- gradlew.bat发出:git status

\n
\xe2\x9d\xaf git checkout HEAD -- gradlew.bat\n\xe2\x9d\xaf git status\nOn branch master\nYour branch is up to date with \'origin/master\'.\n\nChanges not staged for commit:\n  (use "git add <file>..." to update what will be committed)\n  (use "git restore <file>..." to discard changes in working directory)\n        modified:   gradlew.bat\n\nno changes added to commit (use "git add" and/or "git commit -a")\n
Run Code Online (Sandbox Code Playgroud)\n

好吧,我直接从 GitHub 下载了文件,并检查了哈希值:

\n
\xe2\x9d\xaf md5sum gradlew.bat\n6b56324406b764fd6c5d4d7d215a3cd7  gradlew.bat\n\xe2\x9d\xaf sha512sum gradlew.bat\nd4fef021e30640670fe20243e4fc4f0336b2f118f8c172c138a8c0c3028c93b12da9479812cede4196401bbc87ce9df89573dbec7378373cafafca6698867f55  gradlew.bat\n
Run Code Online (Sandbox Code Playgroud)\n

与更改的文件标记完全相同git

\n
\xe2\x9d\xaf md5sum gradlew.bat && sha512sum gradlew.bat\n6b56324406b764fd6c5d4d7d215a3cd7  gradlew.bat\nd4fef021e30640670fe20243e4fc4f0336b2f118f8c172c138a8c0c3028c93b12da9479812cede4196401bbc87ce9df89573dbec7378373cafafca6698867f55  gradlew.bat\n
Run Code Online (Sandbox Code Playgroud)\n

LF这意味着它甚至与/行结尾无关CRLF

\n

git diff也没有帮助,因为它只是表明文件完全改变了:

\n
diff --git a/gradlew.bat b/gradlew.bat\nindex ac1b06f..107acd3 100755\n--- a/gradlew.bat\n+++ b/gradlew.bat\n@@ -1,89 +1,89 @@\n-@rem\n-@rem Copyright 2015 the original author or authors.\n-@rem\n-@rem Licensed under the Apache License, Version 2.0 (the "License");\n-@rem you may not use this file except in compliance with the License.\n-@rem You may obtain a copy of the License at\n-@rem\n-@rem      https://www.apache.org/licenses/LICENSE-2.0\n-@rem\n-@rem Unless required by applicable law or agreed to in writing, software\n-@rem distributed under the License is distributed on an "AS IS" BASIS,\n-@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n-@rem See the License for the specific language governing permissions and\n-@rem limitations under the License.\n-@rem\n-\n-@if "%DEBUG%" == "" @echo off\n-@rem ##########################################################################\n-@rem\n-@rem  Gradle startup script for Windows\n-@rem\n-@rem ##########################################################################\n-\n-@rem Set local scope for the variables with windows NT shell\n-if "%OS%"=="Windows_NT" setlocal\n-\n-set DIRNAME=%~dp0\n-if "%DIRNAME%" == "" set DIRNAME=.\n-set APP_BASE_NAME=%~n0\n-set APP_HOME=%DIRNAME%\n-\n-@rem Resolve any "." and ".." in APP_HOME to make it shorter.\n-for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi\n-\n-@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\n-set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"\n-\n-@rem Find java.exe\n-if defined JAVA_HOME goto findJavaFromJavaHome\n-\n-set JAVA_EXE=java.exe\n-%JAVA_EXE% -version >NUL 2>&1\n-if "%ERRORLEVEL%" == "0" goto execute\n-\n-echo.\n-echo ERROR: JAVA_HOME is not set and no \'java\' command could be found in your PATH.\n-echo.\n-echo Please set the JAVA_HOME variable in your environment to match the\n-echo location of your Java installation.\n-\n-goto fail\n-\n-:findJavaFromJavaHome\n-set JAVA_HOME=%JAVA_HOME:"=%\n-set JAVA_EXE=%JAVA_HOME%/bin/java.exe\n-\n-if exist "%JAVA_EXE%" goto execute\n-\n-echo.\n-echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%\n-echo.\n-echo Please set the JAVA_HOME variable in your environment to match the\n-echo location of your Java installation.\n-\n-goto fail\n-\n-:execute\n-@rem Setup the command line\n-\n-set CLASSPATH=%APP_HOME%\\gradle\\wrapper\\gradle-wrapper.jar\n-\n-\n-@rem Execute Gradle\n-"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*\n-\n-:end\n-@rem End local scope for the variables with windows NT shell\n-if "%ERRORLEVEL%"=="0" goto mainEnd\n-\n-:fail\n-rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of\n-rem the _cmd.exe /c_ return code!\n-if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1\n-exit /b 1\n-\n-:mainEnd\n-if "%OS%"=="Windows_NT" endlocal\n-\n-:omega\n+@rem\n+@rem Copyright 2015 the original author or authors.\n+@rem\n+@rem Licensed under the Apache License, Version 2.0 (the "License");\n+@rem you may not use this file except in compliance with the License.\n+@rem You may obtain a copy of the License at\n+@rem\n+@rem      https://www.apache.org/licenses/LICENSE-2.0\n+@rem\n+@rem Unless required by applicable law or agreed to in writing, software\n+@rem distributed under the License is distributed on an "AS IS" BASIS,\n+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n+@rem See the License for the specific language governing permissions and\n+@rem limitations under the License.\n+@rem\n+\n+@if "%DEBUG%" == "" @echo off\n+@rem ##########################################################################\n+@rem\n+@rem  Gradle startup script for Windows\n+@rem\n+@rem ##########################################################################\n+\n+@rem Set local scope for the variables with windows NT shell\n+if "%OS%"=="Windows_NT" setlocal\n+\n+set DIRNAME=%~dp0\n+if "%DIRNAME%" == "" set DIRNAME=.\n+set APP_BASE_NAME=%~n0\n+set APP_HOME=%DIRNAME%\n+\n+@rem Resolve any "." and ".." in APP_HOME to make it shorter.\n+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi\n+\n+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\n+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"\n+\n+@rem Find java.exe\n+if defined JAVA_HOME goto findJavaFromJavaHome\n+\n+set JAVA_EXE=java.exe\n+%JAVA_EXE% -version >NUL 2>&1\n+if "%ERRORLEVEL%" == "0" goto execute\n+\n+echo.\n+echo ERROR: JAVA_HOME is not set and no \'java\' command could be found in your PATH.\n+echo.\n+echo Please set the JAVA_HOME variable in your environment to match the\n+echo location of your Java installation.\n+\n+goto fail\n+\n+:findJavaFromJavaHome\n+set JAVA_HOME=%JAVA_HOME:"=%\n+set JAVA_EXE=%JAVA_HOME%/bin/java.exe\n+\n+if exist "%JAVA_EXE%" goto execute\n+\n+echo.\n+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%\n+echo.\n+echo Please set the JAVA_HOME variable in your environment to match the\n+echo location of your Java installation.\n+\n+goto fail\n+\n+:execute\n+@rem Setup the command line\n+\n+set CLASSPATH=%APP_HOME%\\gradle\\wrapper\\gradle-wrapper.jar\n+\n+\n+@rem Execute Gradle\n+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*\n+\n+:end\n+@rem End local scope for the variables with windows NT shell\n+if "%ERRORLEVEL%"=="0" goto mainEnd\n+\n+:fail\n+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of\n+rem the _cmd.exe /c_ return code!\n+if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1\n+exit /b 1\n+\n+:mainEnd\n+if "%OS%"=="Windows_NT" endlocal\n+\n+:omega\n
Run Code Online (Sandbox Code Playgroud)\n

我能想到的下一个是权限,但该文件过去是-rwxr-xr-x并且仍然是-rwxr-xr-x.

\n

我试图通过 看看是否还有其他东西stat,但我也没有发现任何线索:

\n
\xe2\x9d\xaf git reset --hard HEAD && stat gradlew.bat && git status && stat gradlew.bat\nHEAD is now at f6d1022 remove irrelevant stuff\n  File: gradlew.bat\n  Size: 2763            Blocks: 8          IO Block: 4096   regular file\nDevice: 259,2   Inode: 7342244     Links: 1\nAccess: (0755/-rwxr-xr-x)  Uid: ( 1000/  <redacted>)   Gid: ( 1000/  <redacted>)\nAccess: 2022-07-05 14:48:48.314141714 +0200\nModify: 2022-07-05 14:48:48.314141714 +0200\nChange: 2022-07-05 14:48:48.314141714 +0200\n Birth: 2022-07-05 14:48:48.314141714 +0200\nOn branch master\nYour branch is up to date with \'origin/master\'.\n\nChanges not staged for commit:\n  (use "git add <file>..." to update what will be committed)\n  (use "git restore <file>..." to discard changes in working directory)\n        modified:   gradlew.bat\n\nno changes added to commit (use "git add" and/or "git commit -a")\n  File: gradlew.bat\n  Size: 2763            Blocks: 8          IO Block: 4096   regular file\nDevice: 259,2   Inode: 7342244     Links: 1\nAccess: (0755/-rwxr-xr-x)  Uid: ( 1000/  <redacted>)   Gid: ( 1000/  <redacted>)\nAccess: 2022-07-05 14:48:48.314141714 +0200\nModify: 2022-07-05 14:48:48.314141714 +0200\nChange: 2022-07-05 14:48:48.314141714 +0200\n Birth: 2022-07-05 14:48:48.314141714 +0200\n
Run Code Online (Sandbox Code Playgroud)\n

我现在没有主意了,是什么导致了这种行为?

\n

tor*_*rek 6

\n

这意味着它甚至与 LF/CRLF 行结尾无关。

\n
\n

啊,但确实如此。

\n

你的存储库是可克隆的,所以我克隆了它。这是文件中的实际内容:

\n
$ git rev-parse HEAD:gradlew.bat\nac1b06f93825db68fb0c0b5150917f340eaa5d02\n$ git cat-file -p ac1b06f93825db68fb0c0b5150917f340eaa5d02 | head -3 | vis\n@rem\\^M\n@rem Copyright 2015 the original author or authors.\\^M\n@rem\\^M\n
Run Code Online (Sandbox Code Playgroud)\n

vis命令显示文件中的内容,确保控制字符(如回车符 (control-M))显示为反斜杠、帽子、字母代码。我们看到该文件实际上具有存储在存储库中的CRLF 结尾。该文件的副本实际上无法更改,因为它位于提交内,并且任何提交的任何部分都无法更改。

\n

奇怪的是,我们发现了以下.gitattributes文件:

\n
$ vis .gitattributes\n* text=auto eol=lf\n*.[cC][mM][dD] text eol=crlf\n*.[bB][aA][tT] text eol=crlf\n*.[pP][sS]1 text eol=crlf\n
Run Code Online (Sandbox Code Playgroud)\n

现在,像这样的有趣的事情.gitattributes是它告诉 Git 弄乱文件数据。棘手的部分是Git 将如何处理文件数据的混乱:

\n
    \n
  • Git 会在复制文件时时,仅对 CRLF 进行 LF 编辑;
  • \n
  • Git 将在从工作树复制文件以压缩它并将其存储在存储库中(在索引中或最终在提交中)时,如果/按照指示进行 CRLF 到仅 LF 编辑。
  • \n
\n

“按照指示”部分很复杂,由 中的规则确定.gitattributes,但您的加起来就是说,对于*.bat文件,Git应该在这两种情况下执行操作。所以它确实:

\n
    \n
  • 在退出时,任何存在的仅 LF 行结尾都会变成 CRLF 结尾。
  • \n
  • 在进入过程中,任何 CRLF 行结尾都会变成仅 LF 结尾。
  • \n
\n

由于提交的文件具有 CRLF 结尾,因此“退出时”不会发生任何情况,但是如果您将文件放回 中,它将更改为以仅 LF 行结尾存储。

\n

我们可以在这里看到这一点。我们首先git ls-files --eol告诉我们索引和工作树中实际有什么,对于存储在 Git 索引中的每个文件:

\n
$ git ls-files --eol\ni/lf    w/lf    attr/text=auto eol=lf   .gitattributes\ni/lf    w/lf    attr/text=auto eol=lf   .gitignore\ni/crlf  w/crlf  attr/text eol=crlf      gradlew.bat\n
Run Code Online (Sandbox Code Playgroud)\n

所以我们看到attr应用到的 sgradlew.battext eol=crlfattr应用于其他文件的 s是text=auto eol=lf.

\n

和的索引和工作树副本仅是 LF。的索引和工作树副本都是 CRLF(两者)。.gitattributes.gitignoregradlew.bat

\n

如果我们现在git add gradlew.bat\xe2\x80\x94 我们可能需要使用--renormalize,具体取决于 Git 年份和某些原始统计数据和计时以及从一个系统到另一个系统不同的许多其他详细信息 \xe2\x80\x94 然后再次运行git ls-files --eol,我们看到的索引版本gradlew.bat已更改:

\n
$ git ls-files --eol\ni/lf    w/lf    attr/text=auto eol=lf   .gitattributes\ni/lf    w/lf    attr/text=auto eol=lf   .gitignore\ni/lf    w/crlf  attr/text eol=crlf      gradlew.bat\n
Run Code Online (Sandbox Code Playgroud)\n

提交此版本将进行一次新的提交,其中永久存储的副本仅具有 LF 行结尾。每次提取都会产生 CRLF 结尾,因为gradlew.batattr/text eol=crlf应用,并且每次git add提取都会将这些 CRLF 结尾更改回仅 LF。

\n

Git 的整个操作区域非常混乱。如果可以不让Git弄乱行结尾,那始终是我的偏好。但是,如果某些文件必须以 CRLF 结尾,那么您编写的样式是我在这里的偏好:存储库.gitattributes中的文件将仅是 LF,但工作树中的文件将是 CRLF 文件。您可能需要执行一次“清理”并提交,这样从那时起,Git 就会对事情感到满意。git add --renormalize .

\n