Git 列出已更改的文件,但没有任何更改

ack*_*ckh 4 windows git

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

我的场景如下:

.gitattributes向现有的 Git 存储库添加了一个文件,其中包含多个已存在的提交。该文件的内容.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
Run Code Online (Sandbox Code Playgroud)

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

git rm --cached -r .
git reset --hard
Run Code Online (Sandbox Code Playgroud)

结果是,Gitgit status现在将 Git 存储库中的大多数文件显示为modified. 但是,我看不到这些文件中的任何更改。diff 工具在文本视图和十六进制视图中均未显示任何更改。

该存储库已在 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
Run Code Online (Sandbox Code Playgroud)

因此,据我从文档中可以解密的内容来看,魔术开关core.autocrlfcore.eol是 Windows 中应该有的开关。

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

tor*_*rek 8

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

\n

从高层次的角度来看,Git 基本上有两个选择:

\n
    \n
  • 永远不要弄乱行结尾。
  • \n
  • 不要弄乱行结尾。
  • \n
\n

第一个非常简单,并且是所有类 Unix 系统上的默认设置。这可能也是 Windows 上的默认设置,但我不使用 Windows,因此我必须遵从其他人的说法。在此设置中,如果您创建一个文件并在该文件中存储字节序列:

\n

h e l l o CTRL-M CTRL-J w o r l d CTRL-M CTRL-J

\n

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

\n
$ printf \'blob 14\\0hello\\r\\nworld\\r\\n\' | shasum\n23eb407b644b0e362fa224168ecd0adfa02b022a\n
Run Code Online (Sandbox Code Playgroud)\n

该文件有 CRLF 行结尾。提取提交将生成一个以 CRLF 行结尾的文件。存储库中的文件现在是只读的,永久冻结;它有 blob hash ID 23eb407b644b0e362fa224168ecd0adfa02b022a,就像任何文件中的每个文件一样一样,只要该文件恰好包含该文本。

\n

现在假设创建了这个文件(或没有),我们打开“混乱行结尾”选项。我们现在有许多子选项,指定Git 将如何、何时、在哪些文件上弄乱行结尾。其中包括eol=crlfeol=lftextbinary等:

\n
*.bat text eol=crlf\n*.sh text eol=lf\n*.jpg binary\n
Run Code Online (Sandbox Code Playgroud)\n

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

\n

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

\n

但对于其他文件来说,情况不再如此。由于 Git 现在正在搞乱他们的行结尾,因此提出和回答更多问题变得很重要:

\n
    \n
  • 具体什么时候会弄乱行结尾?
  • \n
  • Git到底是什么的进行这种混乱的操作时,
  • \n
\n

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

\n

Git 的索引

\n

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

\n

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

\n

然而,关于提交的问题是它们纯粹是只读的。您可以创建新的提交,但您永远无法更改任何现有的提交git commit --amend例如,该命令会伪造它:它不会更改现有的提交,而是创建一个新的提交并停止使用旧的提交,转而使用新的提交。当您无法区分\xe2\x80\x94,有时甚至无法\xe2\x80\x94时,这也一样好。当您能够区分\xe2\x80\x94并且有时您可以\xe2\x80\x94时,裂缝就会显现出来。

\n

但是,如果您无法更改提交\xe2\x80\x94,并且您可以\'t\xe2\x80\x94,并且如果提交内的文件位于特殊的、压缩的、去重的、仅限 Git 的形式,除了 Git 本身之外,其他任何程序都无法读取,那么如何使用提交中的文件呢?答案很简单: 为了使用提交,您必须首先让 Git提取该提交。 我们跑步git checkout还是git switch为了达到这个目的。Git 从提交中提取文件,将它们的可用版本放置在我们的工作树工作树中,我们可以在其中看到它们并完成我们的工作。

\n

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

\n

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

\n

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

\n

不管怎样,在更新索引步骤之后,建议的下一次提交现在已更新。您编辑的文件git add现已暂存,并将git status暂存的哈希 ID 与当前提交的哈希 ID 进行比较,并判断staged for commit这些哈希 ID 是否不匹配。(这意味着,git add对一个已被转回以匹配提交副本的文件会删除staged for commit消息,即使该文件实际上将在下一次提交中。只是哈希 ID 现在匹配了! )

\n

因此,Git 的索引保存了提议的下一次提交。要进行新的提交,您:

\n
    \n
  • 处理工作树中的文件;
  • \n
  • 运行git add它们以将它们复制回 Git 的索引中;和
  • \n
  • 立即运行git commit以打包 Git 索引中的所有内容。
  • \n
\n

这就是为什么每次更改文件时都必须保留git add文件的原因:Git 不会自动将工作树文件复制回索引中。Git 仅在您要求执行此操作时将其复制回来。2

\n

最终效果\xe2\x80\x94 以及下一节中应该考虑的内容\xe2\x80\x94 是,Git 在任何时候都拥有每个文件的三个副本:

\n
  HEAD         index      work-tree\n---------    ---------    ---------\nREADME.md    README.md    README.md\nimg.jpg      img.jpg      img.jpg\nmain.py      main.py      main.py\n
Run Code Online (Sandbox Code Playgroud)\n

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

\n
    \n
  • orgit checkout命令git switch切换到某个提交,将文件提交复制到 Git\ 的索引,然后复制到您的工作树。
  • \n
  • git restore命令从某处读取文件\xe2\x80\x94a提交或索引\xe2\x80\x94,并根据(-S写入暂存)和-W(写入工作树)将其写入索引和/或工作树选项。
  • \n
  • git reset -- file命令从 Git 的索引中读取文件并将其写入您的工作树。(--这里是一个预防措施,以防文件名是,master或者dev类似分支名称的东西)。
  • \n
  • git add file命令从工作树中读取文件并将其写入索引。
  • \n
  • (这里没有列出很多替代方案。)
  • \n
\n

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

\n
\n

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

\n

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

\n
\n

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

\n

如果:

\n
    \n
  • Git 被告知要打乱行结尾,并且
  • \n
  • 一个文件被标记为text,这样 Git就会弄乱这个文件,或者text=auto正在使用该设置,Git猜测这个文件是文本
  • \n
\n

然后:

\n
    \n
  • Git 会选择性地checkout在从索引到工作树(或switchrestore、各种类型等reset)的过程中弄乱文件的字节,并且
  • \n
  • Git在从工作树到索引的过程中弄乱文件的字节(add主要是 )。
  • \n
\n

Git 会搞什么鬼?这取决于eol=设置:

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

    \n
  • \n
  • eol=lf:在退出时 Git 不会对该文件执行任何操作。在 中 Git 会将 CRLF 更改为仅 LF。

    \n
  • \n
\n

这就是 \xe2\x80\x94 这就是 Git 要做的一切!例如,它永远不会在in 的过程中将 LF 更改为 CRLF。从这个意义上说,我们可以说 Git“更喜欢”仅使用 LF 的行结尾。(如果你想要一些更奇特的东西,你可以编写cleansmudge过滤器,它们也分别对“传入”和“传出”的数据进行操作,在这里你可以做任何你喜欢的事情。但是里面内置的东西Git 仅限于这几个 CRLF 选项。)

\n

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

\n

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

\n

结果

\n

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

\n

但假设您有一个存储库,其中部分或所有文本文件均以 CRLF 行结尾提交。这些提交将被永远冻结!你确实无法改变它们。他们将继续使用 CRLF 行结尾。如果您现在开始选择让 Git 处理文件,则未来的提交将逐渐或突然突然出现部分或全部文件仅以 LF 行结尾(存储在存储库中)。

\n

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

\n

事实上,行尾“改变”可以产生 Git 认为是改变的改变。如果存储库中的现有提交具有 CRLF 行结尾,并且您开始让 Git 弄乱行结尾,那么最好进行一次“规范化”提交。 您将成为以这种方式更改的每个文件的每一行的所有者,但是git blame,如果您需要找出某些代码的来源,那么至少有一种方法可以“跳过”特定提交。由于此“修复所有文件,但没有真正的更改”提交除了规范化这些行之外不执行任何操作,因此您可以git blame跳过它。

\n

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

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

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

\n

此处应提及的其他项目

\n

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

\n

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

\n

一般认为,文件的某些变化或特征是不可见难以看到的,这有点奇怪,但我们有非计算示例:例如,在精细排版中,我们有连字符(-),短破折号 (\xe2\x80\x93) 和长破折号 (\xe2\x80\x94)。这些可能会也可能不会在您的计算机上显示为不同宽度的破折号。我们还有其他计算机示例,例如空白编程语言或 makefile 语法的可怕错误(制表符很重要)。并且,在spycraft\xe2\x80\x94中,无论我们是否使用计算机\xe2\x80\x94,我们都有隐写术

\n