我在评论中提到了其中的一些内容,但它需要很大的空间来给出真正的答案来正确地涵盖它。您觉得奇怪的行为之一是:
- Often, 坚定的文件只有 LF 行结尾。
- Often, 工作树文件具有 CRLF 行结尾(Windows 用户往往更喜欢)。
- 这些可能同时成立,但是,
git status
and git diff
不会提及行结尾的任何变化。
这种行为是必要的、适当的。你运行以下命令是错误的:
git checkout master
git diff
and see a lot of diffs!1 But the actual implementation here is very tricky and can result in some apparent weirdness.
有几个关键要素可以帮助您理解这一点,并理解许多其他 Git 行为。您已经提到了其中一些,但让我们更深入地了解细节并看看howGit 管理行结束。我们需要讨论的事情是:
- 文件在提交中存储的方式:我喜欢称之为冻干 format;
- 文件存储在 Git 调用的事物中的方式不同,index or the 暂存区;
- 文件在您的计算机、工作树中的存储方式,您可以在其中查看和处理/使用它们;和
- 它们如何从一种存储格式转换为另一种存储格式。
最后一步是解决行尾问题的关键,但它与其他项目纠缠在一起。
1Nonetheless, sometimes that very thing happens, for other reasons. I'll touch on these here too.
提交存储冻干文件
每次提交都会存储每个文件的完整副本 - 好吧,是提交中的每个文件的完整副本,但这显然是同义反复。这个说法背后的想法是,如果你有文件README.md
and main.py
,比如说,你在你已经完成的地方做了一个新的提交changed main.py
但不是README.md
,新的提交仍然会生成另一个副本README.md
anyway.
显然,每次都重新提交每个文件会极大地浪费磁盘空间。 Git 通过一些巧妙的技巧来避免这种情况。第一个明显的问题是每个存储的文件都是压缩的(与gzip
or bzip
or rar
; Git实际上使用zlib https://zlib.net/压缩)。对于大多数文件,压缩它们可以减少它们占用的空间。典型的源代码压缩得很好。压缩已经压缩的文件往往会适得其反——这是不在 Git 中存储压缩文件的原因之一!——但不会使它们变得足够大而成为问题,因此 Git 只是对所有内容运行 zlib deflate。
The more important trick here, though, is that once Git has frozen a file into a commit, that file is absolutely, totally, 100% read-only. There's a strong technical reason for this, in that Git stores everything—all of what it calls objects—in a simple key-value database, where the keys are hash IDs formed by hashing the value, and the value is the byte-string that is the file's data, prefixed with the object type and size.2 Since the key itself depends on the data, you literally can't change the data: if you try, you get instead a new and different object with a new and different hash ID.3 The old object is still there in the database, with its old key and old stored bytes: the compressed and frozen, i.e., freeze-dried, file is still there.
What this means is that Git never has to store the same file again after all. It can just re-use the file from the previous commit! That is, if we just made a new commit with a new and different main.py
, well, Git had to write the new different main.py
to a new freeze-dried object, but we made it with the same old README.md
, so Git can just re-use the previous freeze-dried README.md
.4
Git 对这些冻干文件的术语是斑点对象。 Blob 和提交是 Git 四种对象类型中的两种。为了完整起见,剩下的两个是tree and 带注释的标签,但我们不需要担心这里的那些。我们只需要查看 blob 对象,因为提交就是retainblob(间接地——通过树对象!),提交对象(轻轻地)。
2The prefix ensures that, e.g., commit <size>\000<commit data>
has a different hash from blob <size>\000<copy of the commit's data>
. Git wants to be able to extract the type from the object, so the fact that you can read out an existing commit and create a file with those contents and store it as a file, means that the type-prefix is necessary.
哈希函数是一种加密函数,部分原因是您不能故意摆弄它来造成冲突,但主要只是为了获得真正良好的哈希分布。强制哈希冲突是理论上可能和could be这对 Git 来说是未来的一个问题,因此 Git 正在转向更长、更安全的哈希。看新发现的 SHA-1 冲突对 Git 有何影响? https://stackoverflow.com/q/42433126/1256452
3Git checks that the hash ID it used to find the object matches the hash of the data, when extracting the data from the object. This acts as a data-corruption test: if the hash of the data, as retrieved by the key, does not match the original key, Git knows that the on-disk data are invalid and tells you that.
4Later, Git compresses these key-value-store objects even further, by taking objects that have been sitting around for a while and packing them into what Git calls a pack file. The objects in a pack file are delta-compressed https://en.wikipedia.org/wiki/Delta_encoding against other objects in that pack file. To do the delta encoding, Git undoes the zlib deflation, finds overlapping byte sequences—there tend to be a lot of these in source code—and builds a delta encoded version that says take the old copy of the file and make these changes to it: a binary, byte-coded variant of what you see as a git diff
. These deltified pack objects then all go into a single pack file. There's a huge amount of effort that goes into deciding what gets deltified against what: it's not just "new version of file vs old version of file".
更高级别的 Git 软件只是说获取哈希 ID 为 H 的对象。如果该对象作为未打包的对象存在,Git 会在重新 zlib 膨胀时获取该对象。否则,Git 将查看每个包文件。如果该对象在那里,Git 可以从它的 deltified 片段中重新组装它,所有这些都来自那个包文件。上一层的代码永远不必知道文件是单个对象还是存储在包中的片段。因此,准确地说,在objectlevel,Git 只进行 zlib 压缩,不进行 delta 压缩。增量编码,如果它发生的话,就会发生below对象级别。
冻干文件重新水合到工作树中
这部分非常简单:只有一个问题,我们将其留到下一节。一次提交是一个快照every文件,但它们都是这种仅 Git 冻干的形式。它们完全被冻结,这对于存档来说很好,但在转换回来之前,甚至无法使用;只要它们被冻结,就无法获得任何东西new完工。因此,它们必须重新水化,就像以前一样:以您的特定操作系统需要的任何方式变回普通文件,存储在普通目录/文件夹中。重新水化冻干的提交文件的结果是工作树。
索引/暂存区
这就是我提到的问题出现的地方。而不是直接提取文件to在你的工作树中,Git 首先将提交提取到 Git 所谓的索引(在某些地方)或暂存区域(在其他文档中)中。索引是什么以及它的作用在合并操作期间变得更加复杂,但在大多数情况下,描述起来很简单:它是提议的下一次提交.
当 Git 进行新的提交时,Git 不会使用工作树。有一些类似于 Git 的版本控制系统do使用工作树作为建议的下一次提交,它们往往更容易使用,但也慢得多。使用这些时,您告诉系统进行新的提交,它实际上会再次冻结每个文件,进入一个新的提交。
另一方面,Git 说:嘿,等等!我们已经冻干了您的大部分文件。代替重新冷冻干燥every文件新的提交,让我们强制你,用户,这样做关于您更改的特定文件,让你跑git add
在他们!所以 Git 首先提取每个文件to索引,然后再重新填充到工作树中。这git add
命令冻结干燥工作树中的文件并将其复制到索引中,替换先前提交中已经存在的文件,或者,如果它是一个新文件,则在索引中创建一个以前不存在的新文件。不管怎样,现在该文件已准备好进入下一次提交......您的所有文件也已准备好didn't git add
。他们还在那里git checkout
,准备进入新的提交。
这就是所有疯狂的地方tracked vs 未追踪的文件来自.跟踪文件很简单现在索引中的任何文件。未跟踪的文件是目前位于工作树中但目前不在索引中的任何文件。您可以随时将一个文件放入索引中:git add file
。您可以随时获取一个文件out现在的索引:git rm file
or git rm --cached file
。使用git rm
将文件取出both索引and工作树,同时使用git rm --cached
仅将文件从索引中取出,不保留工作树文件。
当然,你做的其他事情also修改索引。最明显的一点就是git checkout
经常不得不replace索引,或者至少是它的一部分。这些细节可能会变得非常棘手——请参阅当当前分支上有未提交的更改时签出另一个分支 https://stackoverflow.com/q/22053757/1256452- 但它确实归结为将文件放入索引中,或将它们取出,以及将文件放入工作树中,或取出它们,或者(例如,git rm --cached
or git reset --mixed
)在更改索引中的内容时保留工作树。
无论指数如何变化(或不变),要记住的主要事情是:在任何时候,每个文件最多有三个活动副本:
一份是当前(HEAD
) 犯罪。您可以使用以下命令查看此内容git show HEAD:file
。您根本无法更改此文件,您所能做的就是更改名称的提交HEAD
通过创建新的提交或使用来调用git checkout
移动到不同的提交。
One copy is the freeze-dried one in your index. You can view this with git show :file
or git show :0:file
.5 You can replace it with a new one from your work-tree using git add
.
最后一个副本是工作树中正常的日常读/写副本。您可以对此使用任何常规的非 Git 命令。
I say 最多三个这里因为,例如,当然未追踪的文件不在您的索引中(无论它是否在HEAD
commit),或者从未提交的全新文件可能同时位于索引和工作树中,但不在HEAD
。一般来说,每种情况下有多少个副本应该是显而易见的。
Note that the index actually just holds the blob hash ID of the freeze-dried file, which is already saved in Git's object store. If you commit the file, the blob hash becomes permanent, as the commit itself now uses it. Otherwise the object can eventually expire (though not while its hash remains in the index).6
5The number zero here is the staging number, which has to do with merges. The default number is zero, and except during merge conflicts, everything is always just in staging slot zero—so you can use :0:
or just :
to mean in the index.
6There was a very nasty bug in git worktree add
for a while. The garbage collector did not account for the extra index file, nor the per-worktree refs, associated with each work-tree. It never scanned these extra index files and refs, and if any particular hash appeared in only such an index or ref, Git would sometimes expire such objects, even though the added work-tree needed them! This was fixed in Git 2.15.
行尾、涂抹和清洁过滤器
现在您已经习惯了 Git 始终存储每个文件最多三个副本的想法,now我们可以看到 Git 中的行尾操作是如何工作的。此外,我们可以看到如何定义smudge and clean过滤器及其工作原理。
从冻干形式中取出文件的过程HEAD
提交并放入索引非常简单:Git 只是确定文件的相对路径,例如README.md
or dir1/dir2/file.py
,并在索引的适当位置腾出空间(索引经过精心安排,以便快速访问)并填充关键信息about那里的冻干副本。 Git 还将有关工作树副本的一些信息填充到该文件的索引条目中,我们稍后会看到。
由于索引只保存冻干文件的哈希 ID,因此索引中的内容是exactly如果你现在就提交的话,下一次提交将会是什么。如果索引中的内容来自HEAD
提交,它是exactly里面有什么HEAD
commit.
与所有冻结的、哈希 ID 键控的对象一样,这里没有什么可以改变的。您可以使用新的不同哈希 ID 创建一个新的不同对象,并且由于您可以将新哈希 ID 写入索引,因此您可以批量替换索引副本,但由于您无法将新哈希 ID 填充到现有提交中,您无法更改提交。如果您确实更改了索引,请将其更改为exactly您打算放入下一次提交的内容。
同时,什么进入工作树 is a 复水副本文件的。提交的副本和索引副本被冻干:它们采用仅限 Git 的格式。工作树副本是普通的。有一个转变绝对必须发生在从 Git 中取出,放入工作树中过程,每一次。有一个相应的转换绝对必须发生在git add
冻干文件并将其填充到对象存储和索引中过程,每一次。
所以:为什么不,在转型过程中,also进行行尾过滤吗?这正是 Git 所做的:
将文件从索引复制到工作树(git checkout
,大部分):如果工作树文件应该有 CRLF 行结尾,Git 可以将 blob 中的仅 LF 行结尾转换为工作树中的 CRLF 行结尾。事实上,它可以插入任何你可能想要的任意“脏”东西,通过你的污迹过滤器。一般来说,我们可以将其称为弄脏文件.
将文件从工作树复制到索引(git add
,大多数情况下):如果提交的文件应具有仅 LF 行结尾,Git 可以在写入 blob 对象时将任何 CRLF 结尾转换为仅 LF 结尾。事实上,它可以通过您的过滤器“清除”您在污迹过滤器中添加的任何“污垢”。清洁过滤器。我们可以将其称为清理文件.
Git 在这里提供了三种内置的行尾涂抹和清理模式。如果您想要其他过滤器,则必须编写自己的涂抹和清洁过滤器:
什么也不做:保持索引和工作树匹配。这适用于所有二进制数据。一般来说,它也适用于 Linux 系统,其中行首先不应该有 CRLF 结尾,因此,如果存储库中的所有内容始终与工作树中的所有内容匹配,并且没有任何内容有 CRLF 结尾,则永远不会有任何问题。
在写入工作树上执行 LF-to-CRLF,在写入索引上执行 CRLF-to-LF。这适用于 Windows 用户的某些文本文件。
对 write-to-work-tree 不执行任何操作,但对 write-to-index 执行 CRLF-to-LF。这是 Git 调用的模式input
。在我看来,它并不是特别适合任何事情。这可能就是为什么input
主要是向后兼容的功能。您可以设置相同的模式eol=lf
不过,在`.gitattributes 文件中。
git diff
and git status
与涂抹/清洁/等相比
What git diff
所做的或打算做的主要是:
- 将整个提交与另一个完整提交进行比较;或者
- 将任何提交与建议的下一个提交(即索引)进行比较;或者
- 比较对工作树的任何提交;或者
- 将建议的下一次提交(索引)与工作树进行比较。
其中一些操作专门针对 blob(提交或索引中的冻干文件)进行操作。相对而言,这很容易:它们已经处于它们将永远处于的任何形式。无需进行生产线末端的摆弄、弄脏或清洁。但是,如果行尾过滤器或污迹过滤器有问题,则任何将提交或索引与工作树进行比较的操作都会出现问题。changed工作树中有什么。
有两种明显的方法可以解决这个问题。 Git 可以:
- 清理工作树文件(通过将它们添加到某个位置,例如添加到临时索引),然后比较清理后的文件;或者
- 重新弄脏索引或提交副本(通过将它们提取到某个地方,例如临时文件),然后比较弄脏的文件。
这两个都很慢:它们意味着重新复制every使用这些功能的文件,每次您将某些东西与工作树进行比较。 Git 会在必要时执行此操作(并且根据源代码,它可以执行任一操作 - 我不确定何时会发生哪一个)。但 Git 试图变得更聪明。
如果您刚刚签出一个文件 - 刚刚将其从索引复制到工作树 - 工作树副本must根据定义,匹配索引副本,无论工作树副本有多么“脏”。同样,如果你现在git add
编辑一个文件 - 只是将其从工作树复制到索引 - 索引副本must根据定义,匹配工作树副本,无论索引副本有多“干净”。 Git 在索引中保存了一堆操作系统级别的信息about文件的工作树副本,与文件的索引副本相比。如果这两者匹配,Git 就会assume索引和工作树副本匹配。
请注意,Git在关键情况下保留这个假设,即使不应该。特别是,假设您有一个已提交的文件,该文件仅包含 LF 行结尾,并且您使用以下命令配置了存储库.gitattributes
和/或其他告诉 Git 的设置:无论以哪种方式复制此文件时,请根据复制方向进行 LF / CRLF 转换。从那时起,你改变了.gitattributes
或其他设置,以便 Git 重新提取文件now,它不会做任何事情,如果你git add
文件now,它什么也不做——这会将带有 CRLF 行结尾的文件版本添加到索引中。
Git 会坚持文件的索引和工作树副本匹配,即使它们不再匹配。如果您更改设置back切换到 Git 进行翻译的模式,现在文件再次匹配。在任何时候,Git 都坚持要求文件匹配,因为它使用索引的文件状态信息来绕过真正检查的艰苦工作。
The git status
命令部分包括运行两个git diff
命令,一个进行比较HEAD
到索引,以及将索引与工作树进行比较。第一个 diff 没有行结束问题,因此这里无需担心,但第二个 diff 具有常见的索引与工作树问题。它实际上使用相同的代码git diff
,所以它在认为事物干净与否方面的行为方式是相同的。
git add --renormalize
The git add
命令在某些情况下采用类似的快捷方式。这可以让你做类似的事情git add .
无需 Git 重新压缩和冻干every文件在你的工作树中:它只重新压缩和冷冻干燥基于时间戳等的文件,看起来他们确实需要它。如果您更改了清理设置,这当然会很糟糕,因为当 Git 认为文件已经干净时,文件可能需要一些真正的清理。
The git add --renormalize
操作告诉 Git:击败特殊情况代码。根据操作系统文件时间戳等,不要相信索引和工作树是相同的;真正做到add
,真正应用清洁工艺。因此,如果发生此问题,这是解决此问题的一种简单方法。 (我在 StackOverflow 上看到过有关它无法工作的报告,但从未使用过重现器。)
这些并不是问题的唯一根源
请注意,可以:
- 提交具有实际 CRLF 行结尾的文件
- 稍后,指示 Git 应提取并写入仅使用 LF 行结尾的此类文件
- 进入这样的状态,在提取文件后,不应将其视为“干净”
有时,根据操作系统的变化,尽管 Git 试图巧妙地处理文件时间戳等,但这种情况确实会发生。
但更常见的是,您会看到以下情况:
$ git clone <repo>
$ cd <the-clone>
$ git status
当您在 Windows 或 MacOS 系统上时显示修改的文件,其中您有不区分大小写的本地文件系统,并且您刚刚克隆了在具有区分大小写文件系统的 Linux 系统上编写的存储库。
Linux 用户可以进行包含两个的提交不同的文件其名称仅大小写不同,例如README.MD
and ReadMe.md
。当 Mac 或 Windows 系统上的 Git 将这两个不同的文件提取到工作树时,它通常会首先创建其中一个文件README.MD
——然后去创建另一个,ReadMe.md
,但最终覆盖了内容README.MD
包含已提交的内容(现在已索引复制)ReadMe.md
.
你看到的是修改过的README.MD
,以未修改的ReadMe.md
,因为你的工作树只有一个名为README.MD
与承诺的内容ReadMe.md
.
除了让你的 Linux 同事停止这样做之外,没有什么好的解决方案。 Git 可能应该有一些奇特的方式来处理它,但它没有。它is可以在不启动 Linux 系统的情况下完成此任务,但到目前为止,启动 Linux VM 是最好的方法easiest的方式来处理它。