在 git 拉动时将一个文件从 LF 更新到 CR/LF 的最简单方法?

Easiest way to update one file from LF to CR/LF on git pull?

有很多关于 EOL 转换的问题,但我找不到针对这种特殊情况的答案:我在 Unix 行尾有一个 readme.txt。此文本文件是部署在用户机器上的存储库的一部分,并使用简单的 git pull.

进行更新

我们意识到这个文件应该总是在 CR/LF 中,因此想从 LF 更改它(其他文件都可以)。更新 .gitattributes

readme.txt eol=crlf

有效,但前提是存储库是 克隆的 。如果我想更新它,我必须做

git pull
git rm --cached readme.txt
git reset --hard

即我不能在每个用户的机器上做的事情。有办法解决这个问题吗? readme.txt 的更新对这里有帮助吗?

我一点都不清楚为什么你关心每个用户work-tree中出现的内容。使用 Git 时最重要的是每个 commit 中出现的内容。不过,让我们按要求回答问题:

Would an update to readme.txt help here?

是的,会的。 (这个答案的其余部分都是可选阅读,但可能是个好主意。)

为什么会这样

eol=crlf 属性告诉 Git 当文件被复制时 索引 用户的work-tree、Git 应该在 frozen 格式副本 中找到 \n-only 行结尾,并用 \r\n 行结尾替换它们用户的 work-tree.

并不是说你说的,而是你说的也不。 :-) 事实上,它是不完整的。要真正理解这一点,需要理解提交、索引和用户的 work-tree 是如何相互作用的。

提交

请记住,Git 最基本的目的——它存在的根本原因——是存储提交。每个提交都包含每个文件的完整快照。更准确地说,提交包含该提交中每个文件的完整快照。这么说,这听起来有些多余——但我们的想法是,这相当于那些文件当时存在的 archive。每个提交 可能 有一组完全不同的文件,但这通常不是我们 使用 Git.

的方式

你可以天真地用 rartarzip 之类的存档器构建这样的东西,每次你想提交时,只需一个新的完整档案。每个这样的档案都将完全独立于以前的每个档案。这使他们以后很容易取回。缺点是这些会占用很多space,而且容易忘记。

我们首先观察到每次提交都倾向于 re-use 来自 previous 存档的大部分文件。如果我们不制作 independent 存档,而是尽可能制作一个 re-used 以前的存档呢?事实上,Git 就是这样做的。

为了使这项工作 更快,Git 添加了更多技巧。最主要的是每个文件的数据——它的内容——都存储在压缩的 read-only、Git-only 格式中,这使得查看 Git 是否已经有该文件的副本变得非常快.因为它 read-only——事实上,every 提交的每一部分都是 read-only——所以 re-use 文件的旧副本,基于查找其内容。

我喜欢把这个叫做read-only,Git-only,压缩格式"freeze-dried"。它清楚地表明,您实际上无法 使用 此数据,直到您首先将其恢复为正常的日常格式, "rehydrating" 它。 (即时文件:加水即可!)

索引和你的work-tree

每个文件的提交副本都保存在数据库中。1当您签出时切换到 一些提交,Git 将文件复制出数据库。这会使它们重新水化并发挥作用。

Git 可以到此为止,这两组实体:提交和 work-tree。提交是 read-only,work-tree 是您完成工作的地方。您将从 work-tree 构建 new 提交。其他版本控制系统就是这样做的……但 Git 不会。相反,Git 在当前(或 HEAD)提交和 work-tree 副本之间插入每个文件的 third 副本。

第三个副本——实际上在中间,另外两个之间,所以可能是第二个副本——在freeze-dried 格式,但与提交中的副本不同,您可以更改此副本。更准确地说,您可以替换它。这个中间副本存储在 Git 调用的地方,不同的是 indexstaging area (或者,现在很少, 缓存).2

索引有多个角色——也许是它多个名字的来源——但它的主要角色可以描述为你构建下一个提交的地方制作。由于它开始与您签出的提交相匹配,因此它已经准备好每个文件以进入新的提交。但是假设您以某种方式更改了 work-tree 文件。 如何并不重要,重要的是你改变了它。此 work-tree 文件 尚未 在索引中。

您必须 运行 git add 更新 work-tree 文件。此 将文件 复制 索引,压缩它并将其转换为 freeze-dried 格式。这会将先前的副本从索引中引导出来。现在索引包含更新的文件,索引是准备好进行新的提交。

当您 运行 git commit、Git 收集适当的元数据(您的姓名和电子邮件、日志消息、当前提交哈希 ID 等)并进行最终冻结索引中文件的快照版本。由于这些文件已经处于冻结 格式 ,这个过程非常快,特别是与其他没有令人讨厌的 "index" 的版本控制系统相比。

当您通过转到另一个分支或 "going back in time" 到历史提交来提取不同的提交时,Git 必须更新索引以匹配提交,并更新您的 work-tree以匹配索引。这意味着它必须将每个文件 索引复制到 work-tree,并沿途重新水化。同样,正如我们刚刚看到的,git add 必须从 复制一个文件 到 work-tree, 索引,脱水/freeze-drying 一路走来。这对我们的 crlf 行结尾有几个关键影响,或者更一般地说,对 smudgeclean 过滤器(您还设置了使用 .gitattributes).


1这是Git的对象数据库。文件名存储在 Git 调用的 树对象 中,内容存储在 blob 对象 中,全部由 Git 的 提交对象 。这将各个部分统一在一个大 content-addressable 对象系统中,Git 以一系列提交的形式呈现给您。

2从技术上讲,索引不包含每个文件的实际副本,而是包含模式(+x-x 呈现为 100755100644),一个文件名(完整的嵌入式斜杠:path/to/file.ext),和一个 blob 哈希 。 blob 哈希用于冻结的压缩文件内容:文件数据的 freeze-dried 形式。当数据与任何现有提交中的任何文件的数据匹配时,blob 哈希与现有提交中现有文件的哈希相同。

不过,只要您不使用 git update-indexgit ls-files --stage 了解索引的详细信息,您就可以将其视为额外的副本,在 freeze-dried 格式。其他一切都一样。


过滤,包括行尾

如果在 提取 freeze-dried 数据 过程中,我们 Git 将 newline-only 行结尾替换为 CRLF 行结尾怎么办?这是 "smudging" 过程的一部分:获取一个干净的文件,存储在提交中,现在在索引中,"dirtying it up" 将其放入 work-tree,作为 user-editable, user-usable 文件.

如果在 将常规文件压缩为 freeze-dried 格式 期间,我们 Git 将 CRLF 行结尾替换为 newline-only 行会怎样结局?这是"cleaning"流程的一部分:取一个脏文件,存放在用户的工作区,"clean it"将其放入索引,准备提交。

这就是 eol= 设置 所做的。它们不会,而且 不能 更改任何现有的已提交文件。这些已经在提交中并且一直被冻结。

这也是您的问题所在。

优化

当您从某个提交 a123456... 切换到某个不同的提交 b789abc...、Git 可以:

  1. 从索引中删除索引中的每个文件并 work-tree
  2. re-populate 整个索引和 work-tree 来自新的提交

这会让你得到你想要签​​出的提交。但这会 非常 缓慢并且会对每个文件的 时间戳 产生恼人的副作用。

但是,由于 Git 在提交中存储文件的方式,非常容易 Git 判断某个文件是否名为 [=26] =] 或其他什么, 现在在索引中 因为提交 a1234567... 需要 不同 — 或者完全删除 — 因为path/to/file.ext.

b789abc... 中的内容

如果文件不需要不同,Git就不管它了,在索引和[=244=中] work-tree。如果文件 必须不同,Git 不会让您从当前提交 a123456... 切换到另一个提交 b789abc...除非文件的索引和 work-tree 副本是 "clean",即匹配当前提交。 (这里有很多棘手的角落案例。在 Checkout another branch when there are uncommitted changes on the current branch 上查看更多内容。)

这意味着所有三个副本(HEAD 提交、索引和 work-tree)是否匹配很重要。过滤器和 end-of-line 转换的引入使 匹配 这个词变得棘手。 Git 将查看保存在索引中的文件系统时间戳数据,3 以决定文件是否为 "clean",在某些情况下。

文件的真实 "clean-ness" 部分取决于您选择了哪种 EOL 转换(如果有)。但是,更改 .gitattributes 文件(或更改涂抹和清洁过滤器)并不是 Git 实际上 通知 ,因此如果您更改 EOL 设置,Git 可以认为文件是 "clean"不是,反之亦然。

在您的特定情况下,您向 .gitattributes 添加了一个新设置,表示 当文件从索引复制到 work-tree 时,更改 \n\r\n;当文件从 work-tree 复制到索引时,将 \r\n 更改为 \n 所以如果 Git 注意到,它会检查这些东西......但是Git没注意到。

当拥有现有存储库的用户在提交 H1(对于某些哈希)时,即 master 的提示,并且该用户运行s git pull,他的 Git——我假设用户是男性——在 origin 联系另一个 Git 并获取新的提交。这带来了一个提交,其哈希是 H2(一些其他哈希),这是 originmaster 的尖端。他的 Git 然后 运行s git merge 在散列 ID H2 上将他已经完成的任何 work/commits 与其他工作结合起来。

假设自从 H1H2 以来他没有做过任何工作,他的 H1父提交,他的 Git 做了一个 fast-forward 操作而不是合并,这相当于做了一个 git checkout 的提交 H2 拖拽他的分支名称master 转发指向提交 H2。所以现在 Git 采用了该优化。文件 .gitattributes 有一个不同的 blob 哈希 并且他的索引和 .gitattributes 的 work-tree 副本必须被替换。因为 Git 认为(正确地)这些是干净的,所以它们被替换了。然而,他的 Git 的 readme.txt 索引副本与新提交 H2 具有 相同的 blob 散列。所以他的 Git 不会 触及他的索引或 readme.txt.

的 work-tree 副本

结果如您所见:work-tree 副本继续使用之前的行结尾。

如果两个提交H1H2对文件[=55有不同的内容 =]——注意这意味着不同的 cleaned 内容——那么他的 Git 的 fast-forward 操作将看到他的 readme.txt 的副本,在他的Git的索引和他的work-tree,do需要替换。只要他的 Git 认为他们是 "clean",他的 Git 就会替换他们。这意味着将提交的 readme.txt 复制到索引中,然后将索引副本复制到他的 work-tree 中:此复制将服从新的 eol=crlf 操作并将替换 newline-only "clean frozen file" 数据与 CRLF-ending work-tree 数据。

如果用户随后编辑他的 work-tree readme.txt,他——或者他的编辑,至少——将看到这些 CRLF 结尾。他的编辑如何处理它们取决于他的编辑。 (我强迫我的编辑给我看,然后我把它们去掉,因为我不喜欢它们,我不在乎你想让我拥有它们。:-))如果他更新了文件并且 运行s git add,他的 git add 将去掉那些 CRLF 结尾,用 newline-only 结尾替换它们,文件应该这样;这就是将进入索引的内容,因此将是下一次提交的内容。


3因此索引的 rarely-used 名称 cache。在现代 Git 中,术语 cache 主要是指索引的 in-memory 副本,不过,从索引文件加载然后由任何 Git 命令你 运行ning.