介绍

Git 会统一文本文件的换行符。但是有时这不是想要的行为。

text

This attribute enables and controls end-of-line normalization. When a text file is normalized, its line endings are converted to LF in the repository. To control what line ending style is used in the working directory, use the eol attribute for a single file and the core.eol configuration variable for all text files. Note that setting core.autocrlf to true or input overrides core.eol (see the definitions of those options in git-config[1].

这一段文字已经介绍了核心原理:开启 text 属性后,会将换行符转换为 LF 存储到仓库中。然后使用其他配置控制工作目录中文件的换行符风格,也就是说文件会在检出时进行转换。

哪些文件会被 Git 认为是文本文件?看起来 Git 内部使用猜测方法,看是否会遇到 0 之类的字节决定是否是二进制文件。但是猜测结果有可能不准,另外某些情况下文本文件需要当作二进制文件处理。

有些文件表面上是文本文件,实质上应被作为二进制文件处理。 例如,macOS 平台上的 Xcode 项目会包含一个以 .pbxproj 结尾的文件, 它通常是一个记录项目构建配置等信息的 JSON(纯文本 Javascript 数据类型)数据集,由 IDE 写入磁盘。 虽然技术上看它是由 UTF-8 编码的文本文件,但你并不会希望将它当作文本文件来处理, 因为它其实是一个轻量级数据库——如果有两个人修改了它,你通常无法合并内容,diff 的输出也帮不上什么忙。 它本应被机器处理。因此,你想把它当成二进制文件。

文件类型

文件按照是否可读可以分为文本文件与二进制文件。

并不是所有的文本文件都是人来编写生成的,有大量程序会生成文本文件,只是单纯地将其视作二进制文件,以文本形式保存只是为了人类方便阅读与修改。例如 Unity 资源文件,可以通过选项改为使用文本文件形式存储,那么 Unity 会将文本序列化为 YAML 格式的文件。

如果允许 Git 对这些机器生成的文本文件进行换行符自动转换,如果读取的程序并没有处理不同换行符的问题,那么这就会导致读取出错。

大部分情况下,只需要编写文本文件的程序对换行符进行处理,而不需要 Git 来插手。

全局选项

Git for Windows 安装时有几种选项

  • Checkout Windows-style, commit Unix-style line endings (“core.autocrlf” is set to “true”)
  • Checkout as-is, commit Unix-style line endings (“core.autocrlf” is set to “input”)
  • Checkout as-is, commit as-is (“core.autocrlf” is set to “false”)

推荐在安装时选择第三项,即保持原样检入、保持原样检出,不作换行符自动转换。这样可以保证所有的文件都是修改时的原始状态,而不会被 Git 错误地修改。

自动转换时机

根据 Git 官方文档描述,Git 只会在检入与检出时对文件进行处理。因此 Git 并不会自动将工作目录中正在编写的文本文件自动转换换行符。

那在多人协作时就会出现一些问题,例如文件被设置为使用 LF 换行符,然后本地修改文件换行符为 CRLF,结果在执行 git add test.cs 时提示:

1
2
warning: CRLF will be replaced by LF in test.cs.
The file will have its original line endings in your working directory

并且后续在执行 git status 会发现没有此文件的改动,如果将文件删除并从仓库中恢复(rm test.cs; git checkout test.cs),那么文件就会恢复为 LF 换行符。

也就是说,换行符自动转换发生在检入与检出过程中,具体点说就是 git addgit checkout 操作中。

环境

  • Git for Windows 2.36.0
  • Fork 1.74.1 Windows
  • Windows 10 21H2

问题

Shell 脚本文件要求换行符必须是 LF 才是有效的文件,否则无法执行。

Unity 中 Assets 目录下 .asset 文件以 LF 换行,但是 ProjectSettings 目录下的 .asset 文件以 CRLF 换行。

Unity 中并不是所有的 .asset 文件都是文本文件,地形(Terrain)文件为了减少体积,不管项目的 Serilization Mode 是什么都会强制将 terrain.asset 序列化为二进制文件。

自动转换换行符可能会导致同一个文件在不同条件(操作系统、用户 Git 配置)下大小不同,因为 CRLF 是两个字节,而 CR、LF 是一个字节。

很多人可能會用這個很熱門的 Unity .gitattribute 樣本: https://gist.github.com/nemotoo/b8a1c3a0f1225bb9231979f389fd4f3f

但是它將了 Unity 的 .asset 檔案設定為 LF,而 Unity 有非常多種 .asset 檔案,之前調查過的結果

強制轉換成 LF 會產生問題,底下也有人回報,但是一直沒有更新,所以不建議用這份設定。

思路

核心是禁用 Git 的隐式自动转换,然后显式地指定哪些文件需要自动转换。所以文件的第一行一定是 * -text

方案

第三方方案

有一个开发者个人维护的 gitattributes 模板集,其中包含 Unity 版本 gitattributes/Unity.gitattributes,问题是这个方案是显式地指定已知类型文件的换行符自动转换,并未考虑未知文件类型的换行符自动转换。

有一个不错的 Gist,里面有关于配置的不少讨论:

简单方案

禁用 Git 对所有文件的自动转换换行符,换行符完全由用户自行控制。

1
* -text

注意:虽然可以通过 .editorconfig 文件配置换行符策略,但是在某些不考虑 .editorconfig 的编辑器下依然会产生不同的换行符混合在一个文件中,所以还是推荐在 .gitattributes 中配置自动转换换行符。

精准方案

就是在简单方案的基础上,手动调整指定扩展名文件的换行符。

1
2
3
4
* -text

*.cs text eol=auto
*.sh text eol=lf

推荐看一个现成的仓库配置:

统一换行符

在修改完 .gitattributes 文件后,Git 会读取其中的配置并在之后的检出与检入操作时执行,也就是说只会影响之后的文件改动,仓库中已有文件不受影响。

如果想要统一处理,在修改完 .gitattributes 文件并提交到仓库后,执行以下命令将所有文件重新检出一遍,触发 Git 的自动转换换行符功能,然后将改动的文件提交到仓库中,以统一换行符。

1
2
git rm --cached -r
git reset --hard

方法来自以下英文文章,并且有人翻译了中文版本:

注意:自动转换换行符影响面太大,会影响文件的 blame 结果,因此推荐在项目早期处理。Rider 2022.1 的 blame 会忽略换行符的变化,而是显示最后修改者;而 Fork 的 blame 则不会忽略换行符变化。

GUI 客户端

Fork 与其他类似的 Git 客户端都支持忽略换行符、忽略空白的选项,开启后就看不到所有换行符的修改了。

参考资料