Windows 上的 Git 换行符与 core.autocrlf

太阳作者太阳
原创内容采用 CC-4.0 协议发布,转载请注明出处

我以前把 core.autocrlf 当成一个简单的开关:Windows 设成 true,Linux 设成 false。直到仓库里加了 .gitattributes,格式化器也固定输出 LF,Git 仍然不断提示:

warning: in the working copy of '.editorconfig', CRLF will be replaced by LF the next time Git touches it

更奇怪的是,git status 有时显示上百个文件发生变化,重新加入暂存区以后又恢复干净。这件事如果只盯着 core.autocrlf,很容易越查越乱。Git 实际上同时面对三种状态:提交对象里的内容、索引里的内容和工作区里的文件。换行转换发生在哪一步,取决于路径属性和配置层级。

true 通常不是仓库自己设置的

先分清配置来自哪里。Git for Windows 安装器有一页专门询问换行符策略,默认选项是 CRLFAlways。按照 Git for Windows 的安装参数说明,它对应 core.autocrlf=true

安装器通过 system 配置写入这个值,而不是进入每个仓库修改 .git/configGit for Windows 安装器源码中的配置写入函数也明确使用了 git config --system

所以,在仓库中执行:

git config core.autocrlf

得到 true,只能说明当前生效值是 true,不能证明它来自 local 配置。下面这条命令才会只查询当前仓库:

git config --local --get core.autocrlf

如果它没有输出并返回非零退出码,说明 .git/config 根本没有设置这个选项。

为什么安装器偏向 CRLF

这是一个照顾传统 Windows 工具的兼容性选择。

Windows 长期使用 CRLF 作为文本换行符,Linux 和 Git 仓库通常使用 LF。安装器无法预先知道用户接下来会维护 JavaScript 项目、Visual Studio 工程,还是一批只能识别 CRLF 的旧文件,于是选择了一个偏向 Windows 工作区的默认策略:仓库尽量保留 LF,检出后让本地工具看到 CRLF。

它的目标大致是:

Git 仓库和索引:LF
        ↓ checkout
Windows 工作区:CRLF
        ↓ git add
Git 索引:LF

这个默认值有历史理由,但它不是仓库规范。安装器只看得到操作系统,看不到项目里有没有 Shell 脚本、容器构建、Oxfmt,也不知道团队是否要求所有平台的工作区都保持 LF。

trueinputfalse 分别做什么

Git 的 core.autocrlf 文档说明,true 相当于对所有未明确指定属性的文件使用 text=auto,并把工作区换行设为 CRLF。三个取值的差别可以这样理解:

配置 加入索引时 检出到工作区时 常见用途
true 文本中的 CRLF 规范化为 LF LF 转成 CRLF 希望 Windows 工作区使用 CRLF
input 文本中的 CRLF 规范化为 LF 不做输出转换 Linux、macOS 或只想在提交方向清理换行
false 不根据 core.autocrlf 自动转换 不根据 core.autocrlf 自动转换 .gitattributes 明确管理仓库

最后一行最容易误读。core.autocrlf=false 不是“禁止 Git 做任何换行转换”。它只关闭这个配置带来的自动行为;如果文件命中了 .gitattributes 中的 texteol,Git 仍然会按照属性转换。

为什么不同命令查出来的值不一样

Git 配置不只有一份。常见层级包括:

  • system:Git 安装目录中的系统配置,Git for Windows 安装器通常写在这里;
  • global:当前 Windows 用户的全局配置;
  • local:当前仓库的 .git/config

离仓库越近的配置优先级越高。因此 system 和 global 都可以是 true,而当前仓库通过 local 设置为 false,最终生效值仍然是 false

我现在会直接查看所有来源:

git config --show-origin --show-scope --get-all core.autocrlf

输出可能类似:

system  file:C:/Program Files/Git/etc/gitconfig  true
global  file:C:/Users/USER/.gitconfig            true
local   file:.git/config                         false

再查询一次最终值:

git config --get core.autocrlf

这里应该得到 false。只看 git config --global core.autocrlf,无法判断当前仓库实际采用什么配置。

仓库规则应该写进 .gitattributes

core.autocrlf 是某台电脑上的个人配置,.gitattributes 则会随仓库提交。对于多人参与或需要在 Windows、Linux 之间构建的项目,后者更适合表达仓库规则。

一份常见配置是:

* text=auto eol=lf

*.bat text eol=crlf
*.cmd text eol=crlf

Git attributes 文档说明,text 属性会让文本在加入索引时规范化为 LF,eol 则决定检出到工作区时使用哪一种换行符。上面的配置让普通文本在各平台都使用 LF,Windows 批处理文件仍然使用 CRLF。

如果项目还包含必须保留原始字节的文件,可以明确排除:

*.png -text
*.jpg -text
*.pdf -text

text=auto 本来就会尝试识别二进制文件,这些规则不是所有仓库都必须写。只有自动识别不可靠,或者项目希望把边界写得非常明确时才需要补充。

.editorconfig 管的是另一件事

.gitattributes 决定 Git 如何把内容放入索引和检出工作区,却不能要求编辑器保存时使用哪一种换行符。编辑器应通过 .editorconfig 统一:

root = true

[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true

如果项目使用 Oxfmt,还应让格式化器使用相同的值:

{
  "endOfLine": "lf",
  "insertFinalNewline": true
}

这几份配置看起来重复,处理的阶段却不同:编辑器负责保存文件,格式化器负责重写内容,Git 负责索引和检出。只配置其中一个,其他工具仍可能写出另一种结果。Oxfmt 的其他接入问题,我放在《用 Oxfmt 统一 TypeScript 项目的代码格式》里继续讨论。

警告不等于文件损坏

看到下面这条警告:

CRLF will be replaced by LF the next time Git touches it

它表达的是当前状态与目标策略不同:工作区文件是 CRLF,Git 判断该路径应当规范化为 LF。git add 本身不一定改写工作区文件,但索引会使用规范化后的内容,后续检出或其他 Git 操作可能重写工作区。

反方向的提示:

LF will be replaced by CRLF the next time Git touches it

说明当前策略准备让工作区使用 CRLF,常见原因是 core.autocrlf=true,并且该路径没有更明确的 eol=lf 属性。

偶尔在调整配置后看到一次提示,不算故障。编辑、格式化、加入暂存区时反复来回转换,才说明工具之间没有统一。

git status 干净也可能是 CRLF

Git 比较文本文件时会先按照属性规范化内容。于是索引中是 LF、工作区实际是 CRLF,两边规范化后仍然相同,git status 可以保持干净。

检查物理换行状态可以使用:

git ls-files --eol -- .editorconfig

一种可能的输出是:

i/lf w/crlf attr/text=auto eol=lf .editorconfig
  • i/lf:索引使用 LF;
  • w/crlf:工作区文件当前使用 CRLF;
  • attr/text=auto eol=lf:该路径实际命中的属性。

也可以单独检查属性来源:

git check-attr text eol -- .editorconfig

这两个命令比重复修改 core.autocrlf 更有用。它们回答的是“这个文件现在怎样”和“这个路径应该怎样”。

已有仓库怎样统一

对于使用现代编辑器、格式化器和 Linux 构建环境的跨平台项目,我现在采用下面这组配置:

git config --local core.autocrlf false
* text=auto eol=lf

*.bat text eol=crlf
*.cmd text eol=crlf

同时让 .editorconfig 和格式化器输出 LF。这样 system 或 global 中遗留的 core.autocrlf=true 不必急着删除,仓库级配置会覆盖它,其他依赖 CRLF 的旧仓库也不会受到影响。

如果 .gitattributes 是后来加入的,已有文件不会凭空全部重写。先把业务改动提交或另行妥善保存,确认工作区干净,再执行:

git add --renormalize .
git status --short
git diff --cached --stat
git diff --cached --check

Git 官方也在跨平台换行规范化示例中使用 git add --renormalize .。它会更新暂存区,因此要查看完整暂存差异,并把这次规范化单独提交:

git diff --cached

文件数量很多并不自动意味着代码被改坏,但也不能因为“应该只是换行”就跳过审查。尤其要确认生成文件、二进制文件和有特殊换行要求的脚本是否被正确分类。

安装时应该选哪一个

如果只是给自己的 Windows 电脑安装 Git,而接下来维护的主要是现代跨平台项目,我会在安装器中选择 CRLFCommitAsIs,也就是 core.autocrlf=false,然后让每个仓库通过 .gitattributes 决定换行策略。

如果工作内容依赖要求 CRLF 的传统 Windows 工具,保留安装器默认的 CRLFAlways 也合理。新仓库仍然可以用 local 配置覆盖它。

问题不在于 Git for Windows 为什么至今还默认 true。安装器只能提供一个兼容面较大的起点。真正的问题是把这个起点误认为所有仓库都必须遵守的规则。先看配置来自哪一层,再看具体路径命中了什么属性,换行警告就没有那么神秘了。