Git 的 auto-crlf 设置实际上有什么作用?

What does Git's auto-crlf setting actually do?

我在 Git 中阅读了很多与 auto-crlf 设置有关的其他问题,我不是在问我应该使用哪种 auto-crlf 设置,或者如何规范项目中的行尾。我的问题与了解 auto-crlf 设置本身有关。

快速后台:

我在 Linux 上开始了一个项目,但现在也开始在 Windows 系统上进行研究。在存储库和 Linux 上,我的文件使用 LF 行结尾。然而,尽管在我的 Windows 系统上将 auto-crlf 设置为 "true"(自从我克隆项目之前),Git 认为某些文件 "modified" 如果唯一的区别是行结局。

它只考虑文件 "modified" 如果我打开一个文件,进行更改,保存然后撤消所有更改(CRTL+Z 或手动撤消)并再次保存。我使用的每个 diff 实用程序都告诉我行尾是唯一的区别(回购中的 LF 和本地的 CRLF)。

直到最近,我一直认为此设置会影响除了转换之外哪些文件被视为已修改。但是在结合我正在经历的行为再次阅读描述之后,我开始认为它只在 commit/checkout 上转换行结尾并且与确定哪些文件已被修改无关。

Here 是我阅读此设置说明的地方。

除了处理转换之外,此设置是否还应该影响哪些文件被视为已修改?

编辑:

只是想为任何有类似行为的人添加我的特殊 "Background" 情况。阅读 toreks 的回答后,我能够确定我的 IDE 是 "adding" 文件到 Git 自动保存。这导致 "mtime" 改变,这是 "seemingly" 奇怪行为的根源。

真正的答案是复杂的,涉及 Git 索引的双重性质,它既是 "staging area" 又是 "cache"。

这里也值得考虑 Git 的 涂抹滤镜 清洁滤镜 。从本质上讲,所有 LF/CRLF 转换都是一种涂抹和清洁的形式。

时刻关注三项

无论何时在 Git 存储库中工作,都必须牢记三件事:

  • 当前提交,也称为HEAD。 (文件.git/HEAD存储了部分或全部这些信息:它通常包含分支名称,然后分支名称本身包含其余信息,即当前提交哈希ID。在"detached HEAD" 模式,.git/HEAD 本身包含哈希 ID。)

    根据定义,由于所有提交都是只读的,因此哈希 ID 足以完整地描述它。一旦 Git 将 HEAD 解析为哈希 ID,Git 就可以获取存储的文件。

  • 索引。虽然对索引最好的随意描述是 "what will go into the next commit",但索引的实际形式相当复杂,所以我们暂时不谈细节。这也是索引开始发挥其作用的地方 "cache".

  • 工作树。顾名思义,这是您进行实际工作的地方。它包含所有正常格式的文件,因此您的所有程序和工具都可以使用它们。

    "Normal format" 是这里的关键词:Unix-ish 系统上的正常格式是行以换行符结尾,而某些 Windows 项的正常格式是该行是 CRLF或 '\r\n' 终止。 (我们在这里假设所有 Windows 文件都是这样,尽管实际上只有 大多数 文件是这样,二进制文件是第一个明显的症结所在。)

    如果您考虑污迹和清洁过滤器,工作树中的文件是 "smudged" 形式。也就是说,如果您有类似 Git-LFS 的操作,则允许 Git-LFS 修改文件的工作树版本,使其在某些主要方面与提交的版本不同。 (特别是,Git-LFS 欺骗 Git 只保存一个指向实际文件的指针,然后 Git-LFS 检索真实的——并且可能太大 -GitHub 或其他——来自其他地方的文件,所以你的工作树中的内容实际上根本没有签入!)

请注意,索引位于 "between" 只读 HEAD 提交和工作树中。这意味着文件可以从 HEAD 复制到索引,或从索引复制到工作树,或从工作树复制到索引。 (它们无法从索引复制到 HEAD,除非创建 新提交 ,然后成为当前提交,因为所有提交都是只读的。)

只读提交的文件只能有一种格式

这很明显,但值得说明。如果存储库中的文件具有换行终止格式,则它们与 Windows 的正常格式不匹配。 有东西要来回翻译

如 Pro Git 书中所述,翻译在将文件复制进出索引的过程中完成。但是存在三个这样的可能位置:如果我们从 HEAD 复制到索引,则会将一个(文件的副本)放入索引;如果我们从索引复制到工作树,那就是在那个方向复制;如果我们从工作树复制到索引,就会在另一个方向进行复制。现在索引的实际格式以及我们关心的副本开始变得重要。

索引格式

索引格式复杂。要立即以人类可读的形式查看实际索引,运行 git ls-files --stage --debug 会转储大量信息。 (虽然即使使用 --debug 也省略了一些细节。)最关键和有趣的部分是即使没有 --debug 也能看到的内容,例如:

100644 4646ce575251b07053f20285be99422d6576603e 0       xdiff/xutils.h

第一个值是文件的 "mode"(对于常规文件,总是 100644 或 100755),第二个是 Git 哈希 ID,第三个是阶段号(通常为零), 最后是文件名。

此哈希 ID 至少在最初与原始提交中的哈希 ID 相同。由于该提交的文件是只读的,因此该哈希 ID 代表永久存储形式的文件,而不是其工作树形式。

这反过来意味着文件以 "cleaned" 形式存储在索引中(CRLF 变成 LF-only,或 Git-LFS 将整个文件替换为一个指针)。事实上,清理后的数据已经预先写入 Git 存储库,索引只存储它的 blob 哈希!这是使 Git 快速运行的技巧之一:索引条目只有哈希 ID(以及路径名、模式、阶段编号,以及所有那些 --debug 输出内容)。

这也意味着在复制 from 索引 期间会发生任何涂抹(将 LF 转换为 CRLF,或从 Git-LFS 检索实际文件)工作树。任何清理,将 CRLF 转换为 LF-only 或在 Git 之外存储新文件并更新指针,都发生在复制 from work-tree to 期间索引。

最后,这还意味着 Git 无法仅从工作树文件轻易判断文件的索引版本是否是最新的。工作树版本是否已修改? 确定的唯一方法是进行新的完全清理,并查看结果数据是否获得相同的哈希ID;或者做一个新的完全提取,看看你是否得到相同的工作树文件。但是这个过程慢:它实际上可能需要几十毫秒,即使你不必经过Git- LFS 并在其他地方检索或存储真实文件的副本。乘以许多文件,它太慢了。 (在一个非常大的存储库中,git checkout 的提交实际上可能需要 ,这意味着 git status 和其他此类命令会同样慢。 )

缓存来拯救......有点

Git 对这种性能困境的回答是尽可能完全避免它。 不要实际构建新的存储库实体和散列; 不要 使用现有的存储库对象并重新扩展它。 Git 所做的是将信息 关于 工作树文件存储在索引中:

  ctime: 1500043102:605208000
  mtime: 1500043102:605208000

这两个时间戳分别是"inode change time"和"inode modify time",其中Git是从statlstat系统调用结果中拷贝过来的-树文件。只要底层系统在工作树文件更改时更新工作树时间戳,Git 就可以将工作树文件上的当前时间戳与索引中保存的时间戳进行比较。 (Git 也以相同的方式保存工作树文件大小。)如果时间戳匹配,则文件必须是 "clean"。如果工作树文件上的时间戳比缓存中的时间戳新,则文件可能是脏的,我们必须做额外的工作才能确定。 (In practice, the time stamps on the index file itself also come into play here, since one second is a very long time in compute terms. See this link for details.)

None 这个魔法尊重当前的 CRLF 设置

如果您更改 core.autocrlf 或文件的文本性或污点 and/or 清除某些特定文件的过滤器,这会影响文件从索引中复制的方式到工作树,或从工作树到索引。但对索引文件中存储的缓存数据没有影响。这意味着 Git 会认为(可能是错误的)工作树文件是 "clean",但实际上不是。

It only considers files "modified" if I open up a file, make a change, save and then undo all changes (CRTL+Z or a manual undo) and save again.

写入文件会更改工作树文件上的时间戳,因此 Git 将工作树文件与索引版本进行比较时会做更多的工作。

转化发生时回顾

... I'm starting to think [Git] only converts line endings upon commit/checkout ...

大部分是对的。从 CRLF 到 LF-only 的转换将发生:

  • on git add,它从工作树复制到索引,或任何调用 git add 或其底层代码(包括添加 git commit -agit commit [--only | --include] -- <paths> )
  • 如果文件被标记为这种 "cleaning":它被归类为 text 并且您已为其启用 CRLF 到 LF 转换。

同时,从 LF-only 到 CRLF 的转换发生了:

  • git checkout 上,当它从索引复制到工作树时,或其他一些更模糊的相关情况(例如 git read-tree -u
  • 如果文件被标记为此类 "smudging":它被分类为 text 并且您已为其启用 LF 到 CRLF 转换。

请注意,文件是否以及何时被分类为 text 取决于许多设置。通常,.gitattributes 中的任何内容都会覆盖 core.* 设置,但如果 .gitattributes 中未设置任何内容,则将应用 core.* 设置。

一些其他工具,例如 git showgit cat-file -p,现在可以通过选项进行文本转换(在过去 git show <commit>:<path> 只显示清理过的数据,从来没有弄脏的形式)。很长一段时间以来,git merge 一直支持 "renormalization" 的概念:在对基本提交和两个提交进行差异和组合差异之前进行虚拟签出和签入-合并。