Git 将文件列为已更改但没有更改

Posted

技术标签:

【中文标题】Git 将文件列为已更改但没有更改【英文标题】:Git lists files as changed but there are no changes 【发布时间】:2021-11-19 18:05:14 【问题描述】:

这是非常基本的问题的无数个版本“为什么 Git 告诉我文件已更改但 diff 显示没有更改?”。 here 和 here 已经发布了类似的问题,但这些答案都没有帮助。

我的场景如下:

我在现有的 Git 存储库中添加了一个 .gitattributes 文件,其中包含几个已经存在的提交。 .gitattributes 文件的内容如下:

* text=auto

*.bat text eol=crlf
*.cmd text eol=crlf
*.ps1 text eol=crlf

*.sh text eol=lf

*.csproj   text eol=crlf
*.filters  text eol=crlf
*.props    text eol=crlf
*.sqlproj  text eol=crlf
*.sln      text eol=crlf
*.vcxitems text eol=crlf
*.vcxproj  text eol=crlf

*.cs        text
*.config    text
*.jmx       text
*.json      text
*.sql       text
*.tt        text
*.ttinclude text
*.wxi       text
*.wxl       text
*.wxs       text
*.xaml      text
*.xml       text

*.bmp binary
*.gif binary
*.ico binary
*.jpg binary
*.pdf binary
*.png binary

添加该文件后,我执行了以下命令:

git rm --cached -r .
git reset --hard

结果是 Git git status 现在将 Git 存储库中的大部分文件显示为 modified。但是,我看不到任何这些文件中的任何更改。 diff 工具没有显示任何变化,无论是在文本视图还是十六进制视图中。

该 repo 已在 Windows 机器上创建,我目前正在 Windows 机器上使用它。命令git config --list的输出如下:

http.sslbackend=schannel
diff.astextplain.textconv=astextplain
credential.helper=manager-core
core.autocrlf=true
core.fscache=true
core.symlinks=false
core.editor="C:\\Program Files\\Notepad++\\notepad++.exe" -multiInst -notabbar -nosession -noPlugin
pull.rebase=false
credential.https://dev.azure.com.usehttppath=true
init.defaultbranch=master
user.name=My Name
user.email=my@email.whatever
core.autocrlf=true
core.eol=crlf
diff.tool=bc
difftool.bc.path=C:/Program Files/Beyond Compare 4/bcomp.exe
difftool.bc.cmd="C:/Program Files/Beyond Compare 4/bcomp.exe" "$LOCAL" "$REMOTE"
difftool.bc.prompt=false
merge.tool=bc
mergetool.bc.path=C:/Program Files/Beyond Compare 4/bcomp.exe
mergetool.bc.cmd="C:/Program Files/Beyond Compare 4/bcomp.exe" "$LOCAL" "$REMOTE" "$BASE" "$MERGED"
mergetool.bc.keepbackup=false
mergetool.bc.trustexitcode=true
core.repositoryformatversion=0
core.filemode=false
core.bare=false
core.logallrefupdates=true
core.symlinks=false
core.ignorecase=true

因此,就我可以从文档中解密的内容而言,core.autocrlfcore.eol 的魔术开关应该适用于 Windows。

有人知道我在这里踩到了什么 Git 地雷吗?

【问题讨论】:

你试过运行 git diff 吗?看看有什么区别? @Dmitry 似乎已在第一句中涵盖。 ;) 好的,它不是在第一个,而是在中间。对不起。尝试添加一个文件,提交它,然后在两次提交之间进行文件的差异。你承诺的一个和之前的一个。 也试试 diff --summary 和另一个标志供您试用:--compact-summary 应该为您提供有关文件更改模式的一些有用信息。 【参考方案1】:

这里有多种可能性,但迄今为止最常见的与这些 CRLF 行结尾有关。这很复杂,要真正了解它,我们首先需要一些背景知识。

从高层次上看,Git 基本上有两种选择:

永远不要乱用行尾。 不要乱用行尾。

第一个非常简单,是所有类 Unix 系统的默认设置。它也可能是 Windows 上的默认设置,但我不使用 Windows,所以我不得不听从其他人的意见。在此设置中,如果您创建一个文件并在该文件中存储字节序列:

helloCTRL-M CTRL-Jworl@ d CTRL-M CTRL-J

然后git add 文件并运行git commit,Git 将在存储库中存储该文件包含这 14 个字节的新提交。 Blob 哈希 ID 将为:

$ printf 'blob 14\0hello\r\nworld\r\n' | shasum
23eb407b644b0e362fa224168ecd0adfa02b022a

此文件有 CRLF 行结尾。提取提交将生成一个带有 CRLF 行结尾的文件。存储库中的文件现在是只读的,永远冻结;它具有 blob 哈希 ID 23eb407b644b0e362fa224168ecd0adfa02b022a,就像宇宙中任何地方的 any Git 存储库中的 每个 文件一样,只要该文件包含该文本。

现在假设,已经创建(或没有)这个文件,我们打开“do mess with line endings”选项。我们现在有许多子选项,只指定 如何 Git 将处理行尾、何时、在哪些文件上。其中包括eol=crlfeol=lftextbinary 等:

*.bat text eol=crlf
*.sh text eol=lf
*.jpg binary

这个片段告诉 Git,如果文件名以 .bat 结尾,Git 应该以一种特定的方式处理行尾;如果它以.sh 结尾,Git 应该以另一种特定方式处理行尾;如果它以.jpg 结尾,Git 不应该乱用行尾。

我们知道binary 规范意味着对于此类文件,Git 不会弄乱行尾。这很好,因为例如,.jpg 文件实际上首先没有行,因此任何类似行结尾的东西都只是巧合。当 Git 没有搞砸任何东西时,一切都很简单:Git 会存储那里的内容并向您显示存储的内容。

但其他文件不再是这样。由于 Git 现在弄乱了 他们的 行结尾,因此提出和回答更多问题变得很重要:

究竟 Git 何时会弄乱行尾? Git 究竟做了什么,当它搞砸了?

这就是事情变得复杂的地方。理解这里的关键是了解 Git 的 index。这个东西——这个“索引”——是 Git 的核心,你确实必须了解它才能正确使用 Git,所以让我们来看看这个索引。

Git 的索引

Git 的索引要么非常重要,要么名称太差(或两者兼而有之),以至于它实际上有 三个 名称。它也被称为暂存区,指的是你平时如何使用它,有时也称为缓存。如今,这个姓氏非常罕见:您主要在git rm --cached 之类的标志中看到它。 (有些命令,例如git diff,同时具有 --staged--cached,含义相同。由于某种原因,还没有人愿意添加git rm --staged。我认为现在会发生这种情况,而我仍然认为它总有一天会发生。)

索引为 Git 做了很多事情,但在这里我们真正关心的是它为 做什么以及为你做什么。它为您做的是保留您的提议的下一次提交。 Git 从根本上说不是关于文件,而是关于commits。每个提交拥有个文件:实际上,每个提交都有一个每个文件的完整快照。 (每个提交也有一些元数据,例如提交作者的姓名和电子邮件地址,但我们将在此处跳过。)

不过,提交的问题在于它们是纯只读的。你可以创建新的,但你永远不能更改任何现有的提交。例如,git commit --amend 命令会伪造它:它不会更改现有的提交,而是创建一个新的提交并停止使用旧的提交,转而使用新的提交。当您无法区分时(有时您无法区分),这同样好。当您可以分辨出差异时(有时您可以),裂缝就会显现出来。

但是如果你不能改变一个提交——你也不能——而且如果同样如此,一个提交里面的文件是在一个特殊的,压缩,去重,只有 Git 的形式,除了 Git 本身之外没有任何程序可以读取,你如何使用提交中的文件?答案很简单:为了使用提交,您必须先让 Git 提取该提交。我们运行 git checkoutgit switch 来实现这一点。 Git 从提交中提取文件,将它们的可用版本放在我们的 working treework-tree 中,在那里我们可以看到它们并完成我们的工作。

Git 可以在这里停下来,提交文件(在当前提交中只读,永久冻结)和工作文件。其他版本控制系统确实到此为止。但Git没有。相反,在提取提交时,Git 将每个文件的“副本”放入 Git 的 索引

我在这里将“副本”放在引号中,因为 Git 索引中的文件以内部、压缩、去重的格式存储。因为它们只是从某个提交中提取出来的,所以它们不占用空间:它们被重复数据删除。它们在索引中保存的数据与它们在提交中保存的数据相同:这些数据一直被冻结。

文件的索引“副本”的特别之处在于,与提交的副本不同,您可以替换它们git add 命令告诉 Git:压缩和删除工作树文件。 Git 读取工作树副本,对其进行压缩,并检查压缩结果是否与任何现有提交中的某些现有文件重复。 (这就是 blob 哈希 ID 技巧的用武之地:这就是为什么任何完全由 hello\r\nworld\r\n 组成的文件都有哈希 ID 23eb407b644b0e362fa224168ecd0adfa02b022a。)如果这个重复的,Git 会将重复的哈希 ID 放入索引。如果它不是重复,Git 会安排在对象数据库中存储一个 新 blob1 并将新 blob 的哈希 ID 存储在索引中.

不管怎样,在这个更新索引步骤之后,提议的下一次提交现在被更新了。您git add-ed 的文件现在暂存git status 会将暂存的哈希 ID 与当前提交的哈希 ID 进行比较,如果这些哈希 ID don' 则说 staged for commit不匹配。 (这意味着git add-ing 一个文件已被转回以匹配提交的副本带走 staged for commit 消息,即使该文件实际上将在下一次提交中。它只是哈希 ID 现在匹配!)

所以,Git 的索引保存了这个提议的下一次提交。要进行新的提交,您:

使用工作树中的文件进行操作; 对它们运行git add 以将它们复制回Git 的索引中;和 运行 git commit 以打包 Git 索引中的所有内容就在那时

这就是为什么每次更改文件时都必须保留git adding 的原因:Git 不会自动将工作树文件复制回索引中。 Git 只会在您说要这样做时将其复制回来。2

最终效果(以及您在下一节中应该了解的内容)是,Git 在任何时候都有每个文件的 三个 副本:

  HEAD         index      work-tree
---------    ---------    ---------
README.md    README.md    README.md
img.jpg      img.jpg      img.jpg
main.py      main.py      main.py

例如。工作树版本是 可以查看、读取、写入、提供给 JPG 查看器、使用 Python 程序运行等等的版本。另外两个用于 Git:HEAD 版本是 当前提交 的永久冻结副本,索引版本是可延展但冻结的格式 复制,准备进入 next 提交。

git checkoutgit switch 命令切换到某个提交,将提交的文件out复制到 Git 的索引,然后复制到您的工作树。 git restore 命令从某个位置(提交或索引)读取文件,然后根据-S(写入暂存)和-W(写入工作树)选项。 git reset -- file 命令从 Git 的索引中读取一个文件并将其写入您的工作树。 (此处的 -- 是一种预防措施,以防文件名称为 masterdev 或类似于分支名称的名称)。 git add file 命令从工作树中读取文件并将其写入索引。 (这里没有列出很多替代方案。)

所以所有这些不同的命令都是操纵索引和/或工作树副本的技巧,以准备进行 next 提交(因为 Git 主要是关于进行新的提交,同时保留所有旧的那些)。


1Git 实际上会立即存储新的压缩 blob 对象,即使它在您进行新提交之前被替换。这没关系(如果在某些特殊情况下可能不是最理想的),因为 Git 会不时为您运行 git gc。某些较旧的 Git 版本有一个错误,即 git gc 没有经常运行,这实际上可能是一个问题,但这个问题已经修复多年了。

2使用git add -u 告诉Git 找到修改过的工作树文件并添加它们,这会自动完成这项工作。使用git commit -a 很像运行git add -u && git commit:它在提交之前运行git add -u 步骤。然而,-a 使事情变得复杂,并且与编写不佳的预提交钩子交互很糟糕,所以这是一个坏主意。尽量不要依赖它:改用git add -u,以防你有这些错误的提交钩子之一。或者,学会喜欢索引,它可以让你玩git add -p 之类的巧妙技巧,尽管这与编写不佳的预提交挂钩也很糟糕。


Git 如何以及何时会弄乱行尾

如果:

Git 被告知要处理行尾, 一个文件被标记为text,这样Git 弄乱这个文件,或者text=auto设置被使用并且Git 猜测 这个文件是文本

然后:

Git 会可选地在从索引到工作树(checkoutswitchrestore、各种reset 等)的过程中弄乱文件的字节,以及 Git 在从工作树到索引的过程中弄乱文件的字节(主要是add)。

Git 会做些什么呢?这取决于eol= 设置:

eol=crlf:在退出的过程中,Git 会将 LF-only 更改为 CRLF。如果一行在索引中读取hello\n,Git 会将hello\r\n 写入工作树副本。在 in 中,Git 会将 CRLF 更改为 LF-only。如果工作树副本中有一行读取hello\r\n,Git 会将hello\n 写入索引副本。

eol=lf:在输出的路上,Git 将对文件做任何事情。在 in 的途中,Git 会将 CRLF 更改为 LF-only。

就是这样——这就是 Git 所做的一切!例如,它永远不会以 in 的方式将 LF 更改为 CRLF。从这个意义上说,我们可以说 Git“更喜欢”仅 LF 的行尾。 (如果你想要更高级的东西,你可以编写 cleansmudge 过滤器,它们也分别对“在路上”和“在路上”的数据进行操作,并且在这里你可以为所欲为。但 Git 中的内置内容仅限于这几个 CRLF 选项。)

还有一个棘手的问题:Git 努力优化复制索引和工作树的进出。这种尝试通常是正确的,但是如果你切换 Git 是否以及如何处理行尾,它会失败(在它应该复制的时候不复制)。你链接到的技巧,例如你rm .git/index,主要是解决这个问题的方法。这会强制 Git 复制数据,即使在 Git 认为它不需要复制数据的情况下,即使文件的状态已更改(从 -texttext,或 @987654405 @ 到 eol=crlf 或其他)意味着 Git 确实必须复制。

这就是您需要记住的全部内容。剩下的细节可以解决。

后果

假设您有一个存储库,其中每个提交 中包含文本文件,所有提交的副本都具有仅 LF 行结尾。由于这实际上是 Git 的“首选”格式,因此文件已经全部“正常”。如果你选择让 Git 处理文件,所有 future 提交也将有 LF-only 行结尾,并且未来的提交将匹配现有的提交。

但是假设您有一个存储库,其中部分或所有文本文件以 CRLF 行结尾提交。这些提交一直被冻结!你真的无法改变它们。它们将继续具有 CRLF 行结尾。如果您现在开始选择让 Git 乱用文件,未来 提交将逐渐或突然一次性将部分或所有文件以仅 LF 行结尾,存储在存储库中。 p>

无论上述关于现有存储库的哪些陈述是正确的,您的设置,如果您设置它们,影响您查看文件的方式你的工作树,因为要进入你的工作树,Git 必须从提交中提取文件。但是您的文件查看器可能不会向您显示行尾的样子。也就是说,如果您的首选文件查看器显示一条 CRLF 行和一条仅 LF 行相同,则它们看起来相同,即使它们不同。 p>

行尾“改变”可以做出 Git 认为是改变的改变这一事实。如果存储库中的现有提交具有 CRLF 行结尾,并且您开始让 Git 与行结尾混淆,那么最好进行一次“规范化”提交。 您将成为以这种方式更改的每个文件的每一行的所有者,但git blame 至少可以“跳过”特定提交,如果您需要弄清楚在哪里一些代码来自。由于这个“修复所有文件,但没有真正的改变”提交不会做任何事情除了规范这些行,你可以告诉git blame跳过它。

请注意,Git(和 git diff 认为这些行不同,除非您告诉 git diff 忽略某些空白更改:

--ignore-cr-at-eol:比较时忽略行尾的回车。 -w--ignore-all-space:比较行时忽略空格。

(还有其他的;这只是部分列表。)

这里应该提到的其他项目

当 Git 提交文件时,它会同时存储文件的 data 及其“模式”。 Git有两种文件模式,当它显示它们时它调用100644100755,但是git update-index有一个--chmod选项,它分别拼写-x+x。这告诉 Git 在类 Unix 系统或任何其他具有等效系统的系统上,100755+x 文件应在结帐时标记为可执行。

目前大多数 Windows 文件系统都没有等效的文件系统。在这种情况下,Git 尝试保留现有结帐中的 chmod 设置。 rm .git/index 技巧击败了这个“保留旧设置”技巧。因此,在修复行尾问题时,可以更改文件的 mode。这就是为什么最好在更改 CRLF 行尾设置后使用 git add --renormalize,如果您的 Git 支持的话。

存在一些不可见难以看到的文件的更改或特性的一般想法有点奇怪,但我们有非计算示例:例如,在精细排版中,我们有连字符 (-)、短破折号 (-) 和长破折号 (-)。这些可能会或可能不会在您的计算机上显示为不同的 width 破折号。我们还有其他计算机示例,例如 Whitespace programming language 或 makefile 语法的严重错误(其中制表符很重要)。而且,在间谍领域——无论我们是否使用计算机——我们都有steganography。

【讨论】:

非常感谢您的深入解释!对我来说,这样的“规范化”提交是必要的。我的回购可以追溯到几年前我不知道整个行尾问题的时候。所以那里必须有带有 CRLF 行结尾的 blob。在某个时间点,我将autocrlf = true 添加到我的全局.gitconfig 文件中,这解释了为什么现在并非所有文件都受到新添加的.gitattribues 文件的影响。 但是,我仍然不完全确定为什么我看不到包含 blob 的旧 CRLF 和新创建的包含 blob 的 LF 之间的变化。我正在使用 Beyond Compare 向我展示这些更改,但我不知道将 --ignore-cr-at-eol 之类的内容添加为命令的一部分。另一方面,git diff 显示所有行前面有-,然后所有行前面有+。所以,这似乎意味着行尾发生了变化。 我的印象是BC最初是为 Windows编写的,或者至少现在主要 Windows上使用;因此,默认情况下它可能会忽略 CRLF 与 LF。请注意,autocrlf = true(大致?)等同于* text=auto eol=crlf,但由于 Git 非常努力地避免重新扫描文件,如果可能的话,您最终会得到很多保留其提交中 CRLF 的文件行尾。 检查 Git 存储库中真正内容的一种方法是使用 git cat-file -p 按哈希 ID 提取 blob(根据需要使用 git ls-tree 来查找它们之后),然后在那些老式的 hexdump-and-text 查看器之一(例如di-mgt.com.au/hexdump-for-windows.html)。 crlf 结尾是 0d 0a,LF-only 结尾只是 0a。

以上是关于Git 将文件列为已更改但没有更改的主要内容,如果未能解决你的问题,请参考以下文章

如何撤消 git 中的最后一次提交,但保持我的更改未暂存?

Git将“未更改”文件添加到舞台

git:比较更改

如何可靠地跟踪已部署网站上的更改?

git rebase 删除仅包含行尾更改的提交

(Git) 如何推送未做任何更改的文件?