git diff 没有差异,但由于行结尾的改变,不应该有一些差异吗?

enb*_*bma 3 git

我的问题可能来自于对 Git 某些方面的误解。当我因 Windows 机器上的更改而在 Mac 上将 CRLF 更改为 LF 行结尾时,我想到了这个问题。

1) 我首先在 OSX 上初始化一个新存储库,并将所有文件放入受 CRLF 行结尾影响的文件中。

2)第一次提交,由于设置了 core.autocrlf = input ,git 自动将行结尾更改为 LF

我的本地工作树中的文件仍然具有 CRLF 行结尾,但这里也提供了解决方案(How to Normalizeworking tree lineendings in Git?):

删除Index中的文件,并根据上次commit恢复索引+工作树:

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

现在出现了混乱:我的第一个提交 1) 包含转换后的 LF 行结尾,而我的本地树和索引则不包含。因此,我的期望是 git 应该显示工作树/索引和存储库之间的差异。但

git diff HEAD
git diff --chached
Run Code Online (Sandbox Code Playgroud)

没有列出任何更改?

tor*_*rek 5

我在评论中提到了其中的一些内容,但它需要很大的空间来给出真正的答案来正确地涵盖它。您觉得奇怪的行为之一是:

\n\n
    \n
  • 通常,提交的文件只有 LF 行结尾。
  • \n
  • 通常,工作树文件具有 CRLF 行结尾(Windows 用户往往更喜欢)。
  • \n
  • 这些可以同时为真,但git status不会git diff提及行结尾的任何变化。
  • \n
\n\n

这种行为是必要的、适当的。你运行以下命令是错误的:

\n\n
git checkout master\ngit diff\n
Run Code Online (Sandbox Code Playgroud)\n\n

并看到很多差异!1 但这里的实际实现非常棘手,并且可能会导致一些明显的奇怪现象。

\n\n

有几个关键元素可以帮助您理解这个\xe2\x80\x94 以及理解许多其他 Git 行为。您已经提到了其中一些,但让我们更深入地了解细节,看看Git如何管理行结束。我们需要讨论的事情是:

\n\n
    \n
  • 文件在提交中存储的方式:我喜欢称之为冻干格式;
  • \n
  • 文件在 Git 调用的对象(索引暂存区域)中的存储方式;
  • \n
  • 文件在您的计算机、工作树中的存储方式,您可以在其中查看和处理/使用它们;和
  • \n
  • 它们如何从一种存储格式转换为另一种存储格式。
  • \n
\n\n

最后一步是解决行尾问题的关键,但它与其他项目纠缠在一起。

\n\n
\n\n

1尽管如此,有时由于其他原因,这种情况也会发生。我也会在这里谈到这些。

\n\n
\n\n

提交存储冻干文件

\n\n

每个提交都存储每个文件\xe2\x80\x94以及提交中每个文件的完整副本,但这显然是同义反复。此声明背后的想法是,如果您有文件README.mdmain.py,并且您在已更改 main.py但未更改的地方进行了新提交README.md,则新提交仍然会创建另一个副本README.md

\n\n

显然,每次都重新提交每个文件会极大地浪费磁盘空间。Git 通过一些巧妙的技巧来避免这种情况。第一个明显的问题是每个存储的文件都被压缩(与gziporbzip或一样rar;Git 实际上使用zlib压缩)。对于大多数文件,压缩它们可以减少它们占用的空间。典型的源代码压缩得很好。压缩已经压缩的文件往往会适得其反\xe2\x80\x94这是不在 Git 中存储压缩文件的一个原因!\xe2\x80\x94但不会使它们变得足够大而成为这里的问题,所以 Git 只运行 zlib对一切都泄气。

\n\n

不过,这里更重要的技巧是,一旦 Git 将文件冻结到提交中,该文件绝对、完全、100% 只读。这有一个强有力的技术原因,因为 Git 将所有内容\xe2\x80\x94 以及它所谓的对象\xe2\x80\x94 存储在一个简单的键值数据库中,其中键是通过散列值形成的散列 ID,该值是文件数据的字节字符串,以对象类型和大小为前缀。2 由于密钥本身取决于数据,因此您实际上无法更改数据:如果您尝试,您会得到一个具有新的不同哈希 ID 的新的不同对象。3 旧对象仍然存在于数据库中,具有旧密钥和旧存储字节:压缩和冻结(即冻干)的文件仍然存在。

\n\n

意味着Git 永远不必再次存储相同的文件。它可以重新使用之前提交的文件!也就是说,如果我们只是使用新的和不同的进行了新的提交main.py,那么,Git 必须将新的不同写入main.py新的冻干对象,但我们使用相同的旧对象进行了提交README.md,因此 Git 可以重新使用之前冻干的README.md4

\n\n

Git 对这些冻干文件的术语是blob 对象。Blob 和提交是 Git 四种对象类型中的两种。为了完整起见,剩下的两个是带注释的标签,但我们不需要在这里担心这些。我们只需要查看 blob 对象,并且因为提交是保留blob 的东西(通过树对象间接 \xe2\x80\x94!),所以(轻轻地)提交对象。

\n\n
\n\n

2前缀确保,例如,commit <size>\\000<commit data>具有与 不同的哈希值blob <size>\\000<copy of the commit\'s data>。Git 希望能够从对象中提取类型,因此您可以读取现有提交并使用这些内容创建一个文件并将其存储为文件,这意味着类型前缀是必要的。

\n\n

哈希函数是一种加密函数,部分原因是您不能故意摆弄它来创建冲突,但主要只是为了获得真正良好的哈希分布。强制哈希冲突在理论上是可能的,并且可能成为Git 未来的一个问题,因此 Git 正在转向更长、更安全的哈希。请参阅新发现的 SHA-1 冲突如何影响 Git?

\n\n

3从对象中提取数据时,Git 检查用于查找对象的哈希 ID 是否与数据的哈希相匹配。这充当数据损坏测试:如果通过密钥检索的数据散列与原始密钥不匹配,Git 就知道磁盘上的数据无效并告诉您这一点。

\n\n

4后来,Git 进一步压缩这些键值存储对象,将已经放置了一段时间的对象打包到 Git 所谓的包文件中。包文件中的对象针对该包文件中的其他对象进行增量压缩。为了进行 delta 编码,Git 取消了 zlib 紧缩,找到重叠的字节序列\xe2\x80\x94,源代码中往往有很多这样的字节序列\xe2\x80\x94,并构建一个 delta 编码版本,表示采用旧副本文件并对其进行以下更改:您所看到的git diff. 然后,这些详细的包对象全部进入一个包文件中。需要付出巨大的努力来决定哪些内容与哪些内容相对应:这不仅仅是“文件的新版本与文件的旧版本”。

\n\n

更高级别的 Git 软件只是说给我哈希 ID H 的对象。如果该对象作为未打包的对象存在,Git 会在重新 zlib 膨胀时获取该对象。否则,Git 将查看每个包文件。如果该对象在那里,Git 可以从它的 deltified 片段中重新组装它,所有这些都来自那个包文件。上一层的代码永远不必知道文件是单个对象还是存储在包中的片段。因此,准确地说,在对象级别,Git 只进行 zlib 压缩,而不进行 delta 压缩。增量编码(如果确实发生)发生在对象级别以下。

\n\n
\n\n

冻干文件重新水合到工作树中

\n\n

这部分非常简单:只有一个问题,我们将其留到下一节。提交是每个文件的快照,但它们都采用仅限 Git 的冻干形式。它们完全被冻结,这对于存档来说很好,但在转换回来之前,甚至无法使用;只要它们被冻结,就无法完成任何工作。因此,它们必须重新水化,就像以前一样:以您的特定操作系统需要的任何方式变回普通文件,存储在普通目录/文件夹中。重新水化冻干的提交文件的结果是工作树。

\n\n

索引/暂存区

\n\n

这就是我提到的问题所在。Git 不是直接将文件提取工作树,而是首先将提交提取到 Git 所谓的索引(在某些地方)或暂存区域(在其他文档中)。索引的含义和作用在合并操作期间变得更加复杂,但在大多数情况下,描述起来很简单:它是建议的下一次提交

\n\n

当 Git 进行新的提交时,Git 不会使用工作树中的内容。有一些类似于 Git 的版本控制系统确实使用工作树作为建议的下一次提交,并且它们往往更容易使用,但也慢得多。使用这些时,您告诉系统进行新的提交,它实际上会再次冻结每个文件,进入新的提交。

\n\n

另一方面,Git 说:嘿,等等!我们已经冻干了您的大部分文件。不要在新提交上重新冷冻干燥每个文件,而是让您(用户)通过让您运行它们来强制您(用户)对您更改的特定文件git add执行此操作! 因此,Git 首先将每个文件提取索引,然后再重新放入工作树中。该git add命令冻结工作树中的文件并将其复制到索引中,替换先前提交\xe2\x80\x94中已经存在的文件,或者,如果它是新文件,则在中创建一个新文件以前不存在的索引。不管怎样,现在该文件已准备好进入下一次提交……所有您未提交的 git add文件也已准备好。它们仍然存在git checkout,准备进入新的提交。

\n\n

这就是所有关于跟踪文件与未跟踪文件的疯狂的来源。跟踪文件就是当前位于索引中的任何文件。未跟踪的文件是当前位于工作树中但当前不在索引中的任何文件。 您可以随时将一个文件立即放入索引中:. 您可以随时从索引中取出一个文件:或. using会将文件从索引和工作树中取出using只会将文件从索引中取出,而不会影响工作树文件。git add filegit rm filegit rm --cached filegit rmgit rm --cached

\n\n

当然,你做的其他事情也会修改索引。最明显的一个是git checkout经常需要替换索引,或者至少替换它的一部分。这些细节可能会变得非常棘手\xe2\x80\x94see当当前分支\xe2\x80\x94上有未提交的更改时签出另一个分支但它实际上都归结为将文件放入索引,或将它们取出,以及将文件放入工作树中,或将其取出,或(例如,git rm --cachedgit reset --mixed)在更改索引中的内容时保留工作树。

\n\n

无论索引如何更改\xe2\x80\x94 或不更改\xe2\x80\x94,要记住的主要事情是: 在任何时候,每个文件最多有三个活动副本:

\n\n
    \n
  • 一份是当前 ( ) 提交中冻干的一份HEAD。您可以使用 来查看此内容。您根本无法更改此文件,永远\xe2\x80\x94,您所能做的就是通过创建新提交或使用移动到不同的提交来更改名称调用的提交。git show HEAD:fileHEADgit checkout

  • \n
  • 一份是索引中的冻干副本。您可以使用或查看此内容。5 您可以使用工作树将其替换为新的。git show :filegit show :0:filegit add

  • \n
  • 最后一个副本是工作树中正常的日常读/写副本。您可以对此使用任何常规的非 Git 命令。

  • \n
\n\n

我在这里说最多三个,因为例如,当然,未跟踪的文件不在您的索引中(无论它是否在提交中HEAD),或者可能是从未提交的全新文件在索引和工作树中,但不在HEAD. 一般来说,每种情况下有多少个副本应该是显而易见的。

\n\n

请注意,索引实际上只保存冻干文件的blob 哈希 ID ,该文件已保存在 Git 的对象存储中。如果提交文件,blob 哈希将成为永久的,因为提交本身现在使用它。否则,该对象最终可能会过期(尽管其哈希值保留在索引中)。6

\n\n
\n\n

5这里的数字零是暂存编号,与合并有关。默认数字为零,除了合并冲突期间之外,所有内容始终都位于暂存槽 0\xe2\x80\x94 中,因此您可以使用:0:or 只是在索引中:表示。

\n\n

6git worktree add有一段时间出现了一个非常令人讨厌的错误。垃圾收集器没有考虑额外的索引文件,也没有考虑与每个工作树关联的每个工作树的引用。它从不扫描这些额外的索引文件和引用,如果任何特定的哈希出现在这样的索引或引用中,Git 有时会使这些对象过期,即使添加的工作树需要它们!这已在 Git 2.15 中修复。

\n\n
\n\n

行尾、涂抹和清洁过滤器

\n\n

现在您已经习惯了 Git 始终存储每个文件最多三个副本的想法,现在我们可以了解 Git 中的行尾操作是如何工作的。此外,我们还可以了解如何定义污迹清洁过滤器以及它们的工作原理。

\n\n

从提交中的冻干形式中取出文件HEAD并将其放入索引的过程非常简单:Git 只需确定文件的相对路径,例如README.mddir1/dir2/file.py,并在索引中的适当位置腾出空间\ xe2\x80\x94精心安排了索引,以便快速访问\xe2\x80\x94,并将冻干副本的关键信息塞在那里。Git 还将有关工作树副本的一些信息填充到该文件的索引条目中,我们稍后会看到。

\n\n

由于索引仅保存冻干文件的哈希 ID,因此索引中的内容正是一次提交中的内容(如果您现在就提交)。如果索引中的内容来自提交HEAD,那么它正是提交中的内容HEAD

\n\n

与所有冻结的、哈希 ID 键控的对象一样,这里没有任何内容可以更改。您可以使用新的不同的哈希 ID 创建一个新的不同对象,并且由于您可以将新的哈希 ID 写入索引,因此您可以批量替换索引副本,但由于您无法将新的哈希 ID 填充到现有的索引中。提交,您无法更改提交。如果您确实更改了索引,请将其更改为您建议放入下一次提交的内容。

\n\n

同时,进入工作树的是文件的重新水化副本。提交的副本和索引副本被冻干:它们采用仅限 Git 的格式。工作树副本是普通的。每次从 Git 中取出并放入工作树过程时,绝对必须进行一次转换。每次在冻干文件并将其填充到对象存储和索引过程中时,绝对必须发生相应的转换。git add

\n\n

那么:为什么不在转换过程中进行行尾过滤呢? 这正是 Git 所做的:

\n\n
    \n
  • 将文件从索引复制到工作树(git checkout大多数情况下):如果工作树文件应该具有 CRLF 行结尾,Git 可以将 blob 中的仅 LF 行结尾转换为工作树中的 CRLF 行结尾。事实上,它可以通过污迹过滤器插入您可能想要的任意“脏”东西。一般来说,我们可以将其称为污迹文件

  • \n
  • 将文件从工作树复制到索引(git add大多数情况下):如果提交的文件应具有仅 LF 行结尾,Git 可以在写入 blob 对象时将任何 CRLF 结尾转换为仅 LF 结尾。事实上,它可以通过清洁过滤器“清除”您在污迹过滤器中添加的任何“污垢” 。我们可以将其称为清理文件

  • \n
\n\n

Git 在这里提供了三种内置的行尾涂抹和清理模式。如果您想要其他过滤器,则必须编写自己的涂抹和清洁过滤器:

\n\n
    \n
  • 什么都不做:保持索引和工作树匹配。 这适用于所有二进制数据。一般来说,在 Linux 系统上,行不应该以 CRLF 结尾也是合适的,因此,如果存储库中的所有内容始终与工作树中的所有内容匹配,并且没有任何内容具有 CRLF 结尾,那么\ 从来没有任何问题。

  • \n
  • 在写入工作树上执行 LF-to-CRLF,在写入索引上执行 CRLF-to-LF。 这适用于 Windows 用户的某些文本文件。

  • \n
  • 对写入工作树不执行任何操作,但对写入索引执行 CRLF-to-LF。 这就是 Git 调用的模式input。在我看来,它并不是特别适合任何事情。这可能就是为什么input主要是向后兼容功能的原因。eol=lf不过,您可以在 .gitattributes 文件中设置相同的模式。

  • \n
\n\n

git diffgit statusvs 污迹/清洁/等

\n\n

git diff\xe2\x80\x94or 的作用主要是:

\n\n
    \n
  • 将整个提交与另一个完整提交进行比较;或者
  • \n
  • 将任何提交与建议的下一个提交(即索引)进行比较;或者
  • \n
  • 比较对工作树的任何提交;或者
  • \n
  • 将建议的下一次提交(索引)与工作树进行比较。
  • \n
\n\n

其中一些操作专门适用于提交或索引中的 blobs\xe2\x80\x94freeze-dryed 文件。相对而言,这很容易:它们已经处于它们将始终处于的任何形式。无需进行生产线末端的摆弄、弄脏或清洁。但是,如果行尾过滤器或污迹过滤器更改了工作树中的内容,则任何将提交或索引与工作树进行比较的操作都会出现问题

\n\n

有两种明显的方法可以解决这个问题。Git 可以:

\n\n
    \n
  • 清理工作树文件(通过将它们添加到某个地方,例如临时索引),然后比较清理后的文件;或者
  • \n
  • 重新弄脏索引或提交副本(通过将它们提取到某个地方,例如临时文件),然后比较弄脏的文件。
  • \n
\n\n

这两种方法都很慢:它们意味着每次将某些内容与工作树进行比较时,都会重新复制使用这些功能的每个文件。Git 会在必要时执行此操作(并且根据源代码,它可以执行任一\xe2\x80\x94I\ 不确定何时发生哪一个)。但 Git 试图变得更聪明。

\n\n

如果您刚刚签出文件\xe2\x80\x94刚刚将其从索引复制到工作树\xe2\x80\x94,则根据定义,工作树副本必须与索引副本匹配,无论如何工作树副本“污迹斑斑”。类似地,如果您刚刚git add编辑了一个文件\xe2\x80\x94,刚刚将其从工作树复制到索引\xe2\x80\x94,则根据定义,索引副本必须与工作树副本匹配,无论索引副本有多“干净”。与文件的索引副本相比,Git 在索引中保存了一堆有关文件工作树副本的操作系统级别信息。如果这两者匹配,Git 就会假设索引和工作树副本匹配。

\n\n

请注意,Git在关键情况下保留了这一假设,即使它不应该这样做。 特别是,假设您有一个仅包含 LF 行结尾的已提交文件,并且您使用.gitattributes和/或其他设置配置了存储库,这些设置告诉 Git:无论以哪种方式复制此文件时,都应根据方向进行 LF / CRLF 转换。复制。 从那时起,您更改了或其他设置,以便如果 Git现在.gitattributes重新提取文件,它不会执行任何操作,如果您现在提取文件,它将不会执行任何操作\x​​e2\x80\x94,这将添加以 CRLF 行结尾的文件,到索引。git add

\n\n

Git 会坚持文件的索引和工作树副本匹配,即使它们不再匹配。如果您将设置更改Git 进行翻译的模式,现在文件将再次匹配。在任何时候,Git 都坚持要求文件匹配\xe2\x80\x94,因为它使用索引的文件状态信息来绕过真正检查的艰苦工作。

\n\n

git status命令部分包括运行两个git diff命令,一个用于HEAD与索引进行比较,另一个用于将索引与工作树进行比较。第一个 diff 没有行结束问题,因此这里无需担心,但第二个 diff 具有常见的索引与工作树问题。它实际上使用与 相同的代码git diff,因此它在认为事物干净与否方面的行为方式相同。

\n\n

git add --renormalize

\n\n

在某些情况下,该git add命令采用类似的快捷方式。这可以让你做一些事情,比如git add .不需要 Git 重新压缩和冻干工作树中的每个文件:它只根据时间戳等重新压缩和冻干那些看起来确实需要它的文件。如果您更改了清理设置,这当然会很糟糕,因为当 Git 认为文件已经干净时,它们可能需要进行一些真正的清理。

\n\n

git add --renormalize操作告诉 Git:击败特殊情况代码。根据操作系统文件时间戳等,不要相信索引和工作树是相同的;真正做到add,真正应用清洁过程。 因此,如果发生此问题,这是解决该问题的一种简单方法。(我在 StackOverflow 上看到过有关它无法工作的报告,但从未使用过重现器。)

\n\n

这些并不是问题的唯一根源

\n\n

请注意,可以:

\n\n
    \n
  • 提交具有实际 CRLF 行结尾的文件
  • \n
  • 稍后,指示 Git 应提取并写入仅以 LF 行结尾的此类文件
  • \n
  • 进入一种状态,在提取文件后,它不应该被视为“干净”
  • \n
\n\n

有时,根据操作系统的变化,尽管 Git 试图巧妙地处理文件时间戳等,但这种情况确实会发生。

\n\n

但更常见的是,您会看到以下情况:

\n\n
$ git clone <repo>\n$ cd <the-clone>\n$ git status\n
Run Code Online (Sandbox Code Playgroud)\n\n

当您在 Windows 或 MacOS 系统上时显示修改的文件,其中您具有不区分大小写的本地文件系统,并且您刚刚克隆了在具有区分大小写文件系统的 Linux 系统上编写的存储库。

\n\n

Linux 用户可以提交两个不同的文件,其名称仅大小写不同,例如README.MDReadMe.md。当 Mac 或 Windows 系统上的 Git 将这两个不同的文件提取到工作树时,它会首先创建其中一个 \xe2\x80\x94(通常为README.MD\xe2\x80\x94),然后再创建另一个,ReadMe.md,但最终会用README.MD已提交(现在已索引复制)的内容覆盖 的内容ReadMe.md

\n\n

您看到的是一个已修改的文件README.MD和一个未修改的ReadMe.md文件,因为您的工作树只有一个README.MD以已提交的内容命名的文件ReadMe.md

\n\n

除了让你的 Linux 同事停止这样做之外,没有什么好的解决方案。Git 可能应该有一些奇特的方式来处理它,但它没有。您可以在不启动 Linux 系统的情况下解决此问题,但启动 Linux VM 是迄今为止处理此问题的最简单方法。

\n