摘要
在跨平台 Git 协作中,CRLF 与 LF 混用会导致 diff 噪声、合并冲突、CI 失败以及脚本执行异常。仅依赖 core.autocrlf 往往无法得到稳定、可复现的结果。本文给出一套以 .gitattributes 为核心的方案,确保仓库与索引中的行尾符统一为 LF,并说明迁移已有仓库、验证和 CI 强制的完整流程。
1. 问题根源
Windows 默认使用 CRLF,Unix 类系统使用 LF。Git 提供了自动转换机制,但不同平台、不同 Git 版本、不同本地配置会产生不一致行为。典型症状包括:
- 仅行尾符变化的大面积 diff;
- 合并时大量冲突;
- Shell 脚本因 CRLF 导致
bad interpreter; - 二进制文件被错误转换而损坏;
- CI 与本地行为不一致。
2. Git 行尾转换模型
Git 涉及三个位置:
- 工作区:开发者实际编辑的文件;
- 索引:暂存区;
- 仓库对象:提交历史。
理想模型是:仓库和索引中统一为 LF,工作区可按需适配。控制行尾符的主要机制如下:
core.autocrlf:本地自动转换开关;core.eol:未指定eol属性时的默认行尾;core.safecrlf:转换不可逆时警告或阻止;.gitattributes:仓库级属性,优先级高于本地core.*配置。
.gitattributes 中的关键属性:
text:将文件视为文本并启用行尾规范化;text=auto:由 Git 自动判断是否为文本;eol=lf/eol=crlf:指定检出时的行尾,并隐含text;binary:等价于-text -diff,禁止行尾转换和 diff。
3. 推荐方案:以 .gitattributes 为唯一事实源
在仓库根目录创建 .gitattributes,并提交到版本库。推荐模板如下:
# 默认:自动识别文本文件,并在索引与工作区统一使用 LF
* text=auto eol=lf
# 明确要求 LF 的脚本与文本文件
*.sh text eol=lf
*.bash text eol=lf
*.js text eol=lf
*.ts text eol=lf
*.json text eol=lf
*.md text eol=lf
*.yml text eol=lf
*.yaml text eol=lf
*.css text eol=lf
*.html text eol=lf
# Windows 批处理文件保留 CRLF
*.bat text eol=crlf
*.cmd text eol=crlf
# 二进制文件禁止转换
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.ico binary
*.pdf binary
*.zip binary
*.gz binary
*.tar binary
*.woff binary
*.woff2 binary
*.ttf binary
*.eot binary
说明:
* text=auto eol=lf是核心规则。它让 Git 自动识别文本文件,并强制这些文件在索引和工作区中都使用 LF。- 后续规则按顺序覆盖前面的规则。因此
*.bat text eol=crlf可以覆盖默认的 LF。 - 二进制文件必须显式标记为
binary,避免被误判为文本后转换行尾符。 - 如果团队希望 Windows 工作区保留 CRLF,但提交时归一化为 LF,可将默认规则改为
* text=auto,并设置core.autocrlf=false、core.eol=native。不过,这种方式的工作区行为依赖本地配置,一致性不如强制 LF。
4. 本地 Git 配置
.gitattributes 优先级最高,但仍建议统一本地配置,避免隐式转换。推荐所有平台使用:
git config --global core.autocrlf false
git config --global core.safecrlf warn
core.autocrlf=false 禁用 Git 的自动转换,将控制权交给 .gitattributes。core.safecrlf=warn 在转换不可逆时给出警告。若需要更严格,可设为 true,但迁移阶段可能因大量 CRLF 文件而中断。
不建议在 Windows 上使用 core.autocrlf=true。它会在检出时隐式将 LF 转为 CRLF,可能破坏必须使用 LF 的脚本,并与 .gitattributes 的显式规则产生冲突。
5. 迁移已有仓库
如果仓库已存在 CRLF 污染,需要一次性重新规范化。操作前确保工作区干净,避免丢失未提交修改。
# 1. 确认工作区干净
git status --porcelain
# 2. 添加 .gitattributes 并提交
git add .gitattributes
git commit -m "chore: add .gitattributes for line ending normalization"
# 3. 按新属性重新规范化索引
git add --renormalize .
# 4. 检查改动,确认仅涉及行尾符
git status
git diff --cached --stat
git diff --cached --ignore-cr-at-eol
# 5. 提交规范化结果
git commit -m "chore: normalize line endings to LF"
提交后,工作区文件可能仍保留旧行尾符。若希望工作区也统一为 LF,可在干净工作区执行:
git rm --cached -r .
git reset --hard
该命令会从 HEAD 重新检出所有文件,并应用 .gitattributes 规则。执行前必须确认没有未提交更改,否则会被覆盖。团队其他成员也可以选择重新克隆仓库,这是最安全的方式。
6. 验证与 CI 强制
使用 git ls-files --eol 查看索引和工作区的行尾符状态:
git ls-files --eol
输出示例:
i/lf w/lf attr/text=auto eol=lf src/index.js
i/lf w/crlf attr/text=auto eol=lf README.md
其中:
i/表示索引中的行尾;w/表示工作区中的行尾;attr/表示生效的属性。
若 i/ 为 crlf 或 mixed,说明索引中仍存在 CRLF。可在 CI 中加入检查:
if git ls-files --eol | grep -E 'i/(crlf|mixed)' >/dev/null; then
echo "Error: CRLF detected in Git index"
exit 1
fi
这可以防止后续提交再次引入 CRLF。
7. 常见问题
7.1 出现 warning: CRLF will be replaced by LF
该警告表示工作区文件包含 CRLF,Git 准备在提交时将其转换为 LF。如果这符合预期,可以忽略。若希望工作区也使用 LF,请确认 .gitattributes 已生效,并重新检出文件。
7.2 .gitattributes 不生效
检查以下事项:
- 文件是否位于仓库根目录或正确的子目录;
- 是否已提交到仓库;
- 是否执行过
git add --renormalize .; - 工作区是否已重新检出;
- 本地
core.autocrlf是否设置为false。
7.3 二进制文件被转换
确保二进制扩展名已标记为 binary。如果仍有问题,可使用 -text 明确禁止文本处理:
*.bin -text
7.4 子模块
子模块是独立仓库,需要在子模块内部单独配置 .gitattributes 和本地 Git 设置。
7.5 全局配置与仓库配置冲突
.gitattributes 的 eol 属性会覆盖 core.autocrlf 和 core.eol 的检出行为。但为避免未指定属性的文件出现意外转换,仍建议全局设置 core.autocrlf=false。
8. 结论
将 CRLF 归一化为 LF 的可靠方案不是依赖单一 Git 配置,而是建立明确的仓库级规范:
- 使用
.gitattributes声明文本、二进制和特殊文件的行尾规则; - 所有平台统一设置
core.autocrlf=false,避免隐式转换; - 对已有仓库执行
git add --renormalize .并提交; - 在 CI 中检查索引,阻止 CRLF 再次进入仓库。
这套方案可以确保提交历史、索引和跨平台协作中的行尾符行为一致、可预测、可验证。
