您在 评论 中添加的这些信息非常重要:
我只是使用 .gitattributes
,并加上一行代码:*.* text working-tree-encoding=Windows-1251
working-tree-encoding
指令有很多副作用。更多细节请参见 gitattributes 文档,但我稍后会引用该页面中的一个小部分。
这是来自您上面问题的错误消息:
error: failed to encode 'Var.not' from UTF-8 to Windows-1251
这篇文件的内容并非实际以UTF-8数据存储。
在gitattributes文档中列出的陷阱之一是:
例如,Microsoft Visual Studio资源文件(*.rc
)或PowerShell脚本文件(*.ps1
)有时以UTF-16编码。
也许你的Var.not
文件就是这种情况。
无论如何:
我是否错误地编辑和重新保存了我的文件?
是的,这就是working-tree-encoding所做的。要完全准确,我们需要谈论Git如何在内部存储文件,然后将其提取到您的工作树,以便您可以使用它们,或者从您的工作树复制到内部格式。
Git内部:blob对象,或文件如何永久冻结
Git并不是关于
文件,而是关于
提交。每个提交一旦完成,就(大多数情况下)是永久性的和(完全)只读/不可更改的。然而,一个提交
持有文件或者更精确地说,拥有对文件的引用,所以通过存储提交,Git有效地存储文件。
然而,文件在存储时的
形式也很重要。
通常,Git只是承诺一个文件是一堆字节。无论你在文件中存储了什么字节,Git都会帮你取回来。这适用于原始数据文件——对于你在
.gitattributes
中声明为
-text
的文件。如果你不要求Git修改它们,即你不将它们标记为
text
并设置像CRLF行结束符或
working-tree-encoding
的选项,它也适用于
所有文件。但如果你这样做...好吧,首先,让我们继续了解一下字节包文件是如何工作的。
每次提交都会存储每个文件的副本,但使用去重技术!假设您有一千个提交,每个提交有一千个文件。这意味着您需要让Git存储一百万个不同版本的各种文件。但是,其中大多数文件版本是相同的。也就是说,在您的第一个提交中,您可能创建了一个名为README.md的文件。您向文件中添加了一些文本,并将文件放入了第一个提交中。
之后,您使用相同的README.md进行了另外99次提交。然后,您对其进行了一些更改,并使用第二个版本的README.md进行了剩余的900次提交。
提交中的文件(与提交本身一样)在所有时间上都是冻结的。因此,没有必要制作1000个单独的README.md版本。我们只需要两个版本:第一个和第二个。前100个提交都共享第一个README.md。最后900个提交都共享第二个README.md。
为了快速且节省空间地完成这个任务,Git 使用 zlib deflate 对一组字节文件进行压缩,并将其存储在 Git 称之为“blob object”的对象中。每个 blob object 都有一个唯一的哈希 ID,就像每个提交都有一个唯一的哈希 ID 一样。第一个 README.md 的哈希 ID 基于其中的数据字节。第二个 README.md 的哈希 ID 基于该第二个 README.md 中的数据字节。因此,只有两个 blob objects 在所有的 1000 个提交中共享,每个提交都引用具有正确冻结、压缩的 README.md 内容的对象。
所有这些的结果是,每个提交的文件存储都由这些冻结、压缩的 blob objects 组成。我喜欢称这种形式的文件为“freeze-dried”,它们就像冷冻干燥的咖啡一样,需要加水才能恢复原始内容——原始的字节组合。
(原文中的
和标签已保留)
因此,要检查提交,Git必须重新生成所有冷冻干燥的文件。提交保存了冷冻干燥(且不可修改!)的副本。工作树保存常规格式的文件。稍后我们会回到这个问题。
Git内部:索引,又称暂存区
当您进行新提交时,Git必须将所有文件打包为新的或重复使用的冷冻blob对象。其他版本控制系统通过重新冻结每个文件来完成此操作。这很慢!相反,Git做了一些聪明的事情。
当您首次检出某个现有提交时,Git不仅重新生成其文件。Git还存储对现有冷冻干燥副本的引用。列出在当前提交中,以它们的冷冻干燥副本形式存在的哪些文件的列表,在Git中称为索引、暂存区或(现在很少见)缓存。
换句话说,索引列出了提取此提交到工作树中所需的所有blob哈希ID。
当您在工作树中修改文件时,索引不会发生任何变化。您必须对每个修改过的文件运行git add <file>
命令。此git add
步骤将文件从工作树中复制出来,并重新压缩字节以生成内部冷冻格式。如果需要,这将立即创建一个新的blob对象。现在,Git在索引中具有冻结格式的哈希ID,可随时提交。
换句话说,索引始终包含下一个提交,准备就绪。如果要更新的文件在下一个提交中更新,则必须对其运行git add
命令。这将通过查找或创建内部blob对象将文件复制到索引中,一次又一次,索引包含下一个提交,准备就绪。
这也是为什么你必须继续运行“git add”的原因。更新工作树文件不会影响索引,而“git commit”将从索引中创建新的提交。如果它不在索引中,则不会包含在新提交中。无论在索引中的是什么,都会出现在新提交中。请注意,“git status”的工作方式:
将HEAD
提交与索引进行比较。无论哪些文件是不同的,Git都会说已暂存以便提交。当两个文件相同时——也就是它们是相同的blob对象时——Git什么也不说。
将索引与工作目录进行比较。每当工作目录中的文件不同时,Git会说未暂存以便提交。当两个文件相同时(在适当的重新水化或冷冻干燥后),Git什么也不说。(请注意,有两种比较方式:冷冻并比较冷冻的,或重新水化并比较重新水化的。我认为Git使用了后者,因为各种原因,但文档没有做出承诺,所以可能会毫无预警地改变。)
因此,索引或暂存区实际上是被提交的内容。您的工作目录只是为了让您使用文件。这些文件实际上永远不会被提交:提交的是索引中的冷冻干燥物。
.gitattributes
会影响冷冻干燥和重新水化过程
注意,每当一个文件从Git中出来时,都必须重新被再次解压。注意,每当一个文件进入索引/暂存区时,都必须被冷冻干燥。这些过程总是会处理字节文件,通过使用zlib deflate进行压缩或使用zlib inflate进行重新生成,以达到最佳效果。zlib deflate/inflate是一种数据保留操作:在往返(deflate + inflate)后,它永远不会改变任何字节。
但由于Git已经处理了每个文件的每个字节,这也是改变字节的理想位置。例如,假设我们希望冷冻干燥的文件始终使用换行符结束,但Windows上的工作树文件使用CRLF行结束符。我们可以告诉Git:
- 在重新解压文件时,将`\n`更改为`\r\n`(LF-only to CRLF)。
- 在冷冻干燥文件时,将`\r\n`更改为`\n`(CRLF to LF-only)。
由于 Git 提交是从索引(冷冻干燥)提交的,而不是从工作树(重新水合)提交的,因此这正是我们想要的。为了做到这一点,我们所要做的就是写入:
*.txt text eol=crlf
但我们可以让它不仅仅是LF/CRLF转换。实际上,使用Git所谓的“clean”和“smudge”过滤器,我们可以插入自己的任意操作。(这就是Git-LFS的工作原理)。或者,像这种特殊情况一样,我们可以设置“working-tree-encoding”。
“Working-tree encoding”影响了冷冻干燥和再水化。
“working-tree-encoding”设置告诉Git:
当再水化时,假定原始文件是UTF-8,并重新编码为“working-tree encoding”。
当冷冻干燥时,假定原始文件在“working-tree encoding”中,并在执行通常的zlib deflate之前将其转换为UTF-8。
为了使其正常工作,blob对象必须实际上是UTF-8。此外,此操作——从UTF-8到任何格式,从任何格式到UTF-8——需要保持一致:如果不是,每个提交都可能具有某些随机的重新编码成UTF8。这与deflate/inflate的往返传输思想相同。但并非所有编码都能够提供良好的保证。
关于陷阱的更多信息,比gitattributes文档提到的还要多,请参见Joel on Software: The Absolute Minimum Every Software Developer Absolutely, Positively Must Know About Unicode and Character Sets (No Excuses!)和this article on Unicode combining characters and normalization,其中后者展示了两个看起来相同的字符串(“Zoë”)可能使用不同的字节序列(组合变音符号和字母E,或使用小写带变音符号的Unicode字符)。
在您的情况下,最可能的问题是输入文件一开始就不是UTF-8格式(但也可能是某种重新编码错误)。