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.autocrlf
和 core.eol
的魔术开关应该适用于 Windows。
有人知道我在这里踩到了什么 Git 地雷吗?
【问题讨论】:
你试过运行 git diff 吗?看看有什么区别? @Dmitry 似乎已在第一句中涵盖。 ;) 好的,它不是在第一个,而是在中间。对不起。尝试添加一个文件,提交它,然后在两次提交之间进行文件的差异。你承诺的一个和之前的一个。 也试试 diff --summary 和另一个标志供您试用:--compact-summary 应该为您提供有关文件更改模式的一些有用信息。 【参考方案1】:这里有多种可能性,但迄今为止最常见的与这些 CRLF 行结尾有关。这很复杂,要真正了解它,我们首先需要一些背景知识。
从高层次上看,Git 基本上有两种选择:
永远不要乱用行尾。 不要乱用行尾。第一个非常简单,是所有类 Unix 系统的默认设置。它也可能是 Windows 上的默认设置,但我不使用 Windows,所以我不得不听从其他人的意见。在此设置中,如果您创建一个文件并在该文件中存储字节序列:
h
e
l
l
o
CTRL-M CTRL-Jw
o
r
l
@ 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=crlf
、eol=lf
、text
、binary
等:
*.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 checkout
或git switch
来实现这一点。 Git 从提交中提取文件,将它们的可用版本放在我们的 working tree 或 work-tree 中,在那里我们可以看到它们并完成我们的工作。
Git 可以在这里停下来,提交文件(在当前提交中只读,永久冻结)和工作文件。其他版本控制系统确实到此为止。但Git没有。相反,在提取提交时,Git 将每个文件的“副本”放入 Git 的 索引。
我在这里将“副本”放在引号中,因为 Git 索引中的文件以内部、压缩、去重的格式存储。因为它们只是从某个提交中提取出来的,所以它们不占用空间:它们被重复数据删除。它们在索引中保存的数据与它们在提交中保存的数据相同:这些数据一直被冻结。
文件的索引“副本”的特别之处在于,与提交的副本不同,您可以替换它们。 git add
命令告诉 Git:压缩和删除工作树文件。 Git 读取工作树副本,对其进行压缩,并检查压缩结果是否与任何现有提交中的某些现有文件重复。 (这就是 blob 哈希 ID 技巧的用武之地:这就是为什么任何完全由 hello\r\nworld\r\n
组成的文件都有哈希 ID 23eb407b644b0e362fa224168ecd0adfa02b022a
。)如果这个是重复的,Git 会将重复的哈希 ID 放入索引。如果它不是重复,Git 会安排在对象数据库中存储一个 新 blob,1 并将新 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 add
ing 的原因: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 checkout
或 git switch
命令切换到某个提交,将提交的文件out复制到 Git 的索引,然后复制到您的工作树。
git restore
命令从某个位置(提交或索引)读取文件,然后根据-S
(写入暂存)和-W
(写入工作树)选项。
git reset -- file
命令从 Git 的索引中读取一个文件并将其写入您的工作树。 (此处的 --
是一种预防措施,以防文件名称为 master
或 dev
或类似于分支名称的名称)。
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 会可选地在从索引到工作树(checkout
或switch
、restore
、各种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 的行尾。 (如果你想要更高级的东西,你可以编写 clean 和 smudge 过滤器,它们也分别对“在路上”和“在路上”的数据进行操作,并且在这里你可以为所欲为。但 Git 中的内置内容仅限于这几个 CRLF 选项。)
还有一个棘手的问题:Git 努力优化不复制索引和工作树的进出。这种尝试通常是正确的,但是如果你切换 Git 是否以及如何处理行尾,它会失败(在它应该复制的时候不复制)。你链接到的技巧,例如你rm .git/index
,主要是解决这个问题的方法。这会强制 Git 复制数据,即使在 Git 认为它不需要复制数据的情况下,即使文件的状态已更改(从 -text
到 text
,或 @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有两种文件模式,当它显示它们时它调用100644
和100755
,但是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 将文件列为已更改但没有更改的主要内容,如果未能解决你的问题,请参考以下文章