你的 PR 里本应只有十几行业务改动,但 git diff 显示了两千行变更,reviewer 打开 diff 一看全是「LF → CRLF」或「CRLF → LF」的换行符变化。或者一个 Windows 开发者和一个 macOS 开发者修改了同一个文件,产生了大量”假冲突”。又或者 CI 里 git diff --exit-code 因为换行符问题报出了本不应该有的改动。CRLF 问题不影响程序运行,但会淹没真实的代码 diff,让代码审查变成噩梦,也会让 git blame 历史被大量无意义的格式化 commit 污染。
常见原因
1. Windows 开发者的 core.autocrlf=true 与其他人不一致
Windows 上 Git 默认把 core.autocrlf 设为 true,checkout 时把 LF 转为 CRLF,commit 时再把 CRLF 转回 LF。但若有人设为 false 或者用了 WSL(Linux 子系统),就会直接提交 CRLF 文件,其他人再 checkout 时发生大量换行符转换。
怎么判断:git config --global core.autocrlf 查看本机配置;git diff --whitespace=no-warn HEAD 若差异消失,说明全是空白字符(包括换行符)变化。
2. 仓库没有 .gitattributes 文件,依赖个人 Git 配置
没有 .gitattributes 时,换行符行为完全取决于每个人的本地 Git 配置,团队成员配置不一致,结果是混乱的换行符状态。
怎么判断:cat .gitattributes 2>/dev/null | grep eol 若无输出或文件不存在,说明未统一配置换行符。
3. .gitattributes 里规则有漏洞,部分文件未覆盖
.gitattributes 配置了 *.py text eol=lf,但忘记了 *.sh、*.ts、*.yaml 等其他文本文件,这些未覆盖的文件在 Windows 上依然会被自动转换。
怎么判断:git diff --check 会列出包含混合换行符的文件;file path/to/file 若输出 CRLF line terminators,说明该文件是 CRLF。
4. 编辑器配置不一致(VS Code 与 Notepad 混用)
VS Code 有 files.eol 设置,IntelliJ 有 Line separator 设置,Notepad 永远保存 CRLF。若团队成员用不同编辑器且未统一配置,每次保存文件都可能改变换行符。
怎么判断:git log --oneline -- path/to/file | head -5 查看频繁修改换行符的 commit,记录提交者名字,确认是特定编辑器行为。
5. Shell 脚本在 Windows 上被转成 CRLF,在 Linux/macOS 上执行报错
#!/bin/bash\r 因为 CRLF 的 \r 被当作文件名,脚本无法执行,报错「/bin/bash^M: bad interpreter」。这不只是 diff 问题,还会导致脚本执行失败。
怎么判断:bash script.sh 若报 bad interpreter 或 ^M,说明脚本是 CRLF 格式。
6. 从其他版本控制系统(SVN、TFS)迁移带入了 CRLF
从 SVN 或 Azure DevOps(TFS)迁移到 Git 时,历史里包含了 CRLF 文件,首次 git checkout 在 Linux CI 上触发大量「CRLF will be replaced by LF」的转换,产生意外的脏工作区。
怎么判断:git log --all --oneline | tail -10 查看最早的 commit,若是迁移时导入的,检查那些文件的换行符格式。
最短修复路径
Step 1:在仓库根目录创建或更新 .gitattributes
# .gitattributes — 统一换行符配置
# 默认:所有文本文件存储为 LF
* text=auto eol=lf
# 明确指定文本文件类型
*.py text eol=lf
*.js text eol=lf
*.ts text eol=lf
*.jsx text eol=lf
*.tsx text eol=lf
*.json text eol=lf
*.yaml text eol=lf
*.yml text eol=lf
*.md text eol=lf
*.sh text eol=lf
*.html text eol=lf
*.css text eol=lf
# Windows 批处理脚本保持 CRLF
*.bat text eol=crlf
*.cmd text eol=crlf
# 二进制文件不做任何转换
*.png binary
*.jpg binary
*.gif binary
*.ico binary
*.zip binary
*.pdf binary
Step 2:重新规范化仓库里所有现有文件的换行符
git add --renormalize .
git status
# 若有文件被标记为修改(只是换行符变化),提交这次规范化:
git commit -m "chore: normalize line endings to LF via .gitattributes"
Step 3:清理本地工作区的换行符(重新 checkout)
git rm --cached -r .
git reset --hard HEAD
Step 4:修复已有的 CRLF 文件(不重写历史时)
# 使用 sed 批量转换(Linux/macOS)
find . -name "*.sh" -exec sed -i 's/\r$//' {} \;
# 或使用 dos2unix 工具
brew install dos2unix # macOS
dos2unix **/*.sh
Step 5:统一团队成员的 Git 配置
# 对所有平台推荐:让 .gitattributes 控制换行符,不依赖个人配置
git config --global core.autocrlf false
git config --global core.eol lf
Step 6:在 CI 里验证换行符一致性
# CI 步骤:检查是否有 CRLF 文件混入
git diff --check || (echo "Found whitespace/line-ending issues" && exit 1)
预防建议
- 项目初始化第一步就创建
.gitattributes,比之后补救容易得多,确保从第一个 commit 起换行符就是统一的。 - 在 VS Code 的项目设置(
.vscode/settings.json)里统一配置"files.eol": "\n",强制所有成员使用 LF,不依赖个人设置。 - 在 onboarding 文档里写明:Windows 开发者应设置
git config --global core.autocrlf false,让.gitattributes而非个人配置管理换行符。 - 使用 EditorConfig(
.editorconfig)统一编辑器级别的换行符行为:end_of_line = lf,主流编辑器都支持。 - Shell 脚本在
.gitattributes里强制eol=lf,并在 CI 里加shellcheck检查,CRLF 脚本会被 shellcheck 检测到。 - 合并来自 Windows 环境的 PR 时,reviewer 用
git diff --ignore-cr-at-eol查看真实改动,区分换行符噪音和实际变更。
常见问答 (FAQ)
Q: core.autocrlf=true、input、false 分别是什么行为?
A: true:checkout 时 LF→CRLF,commit 时 CRLF→LF(Windows 开发者用);input:checkout 时不转换,commit 时 CRLF→LF(Unix/macOS 开发者用);false:完全不转换,由 .gitattributes 控制(推荐跨平台团队使用)。
Q: .gitattributes 里 text=auto 和 text eol=lf 有什么区别?
A: text=auto 让 Git 自动检测文件是否为文本,文本文件在检入时规范化,换行符由 core.eol 决定;text eol=lf 强制指定该文件在工作区和仓库里都使用 LF,优先级更高,推荐用于关键文件类型。
Q: 历史里已经有大量 CRLF commit,会影响 git blame 吗?
A: 会。历史里的 CRLF→LF 转换 commit 会让 git blame 里某些行的最后修改者变成「格式化 commit」而不是真正的业务改动者。解决方法:git blame --ignore-rev <format-commit-sha>,或者用 .git-blame-ignore-revs 文件批量忽略格式化 commit。
Q: Windows 上的 WSL(Linux 子系统)会有 CRLF 问题吗?
A: WSL 内的 Git 行为与 Linux 一致,core.autocrlf 默认是 false,不会自动转换换行符。但如果在 Windows 宿主文件系统(/mnt/c/)上操作文件,宿主的换行符配置可能影响文件,建议 WSL 项目放在 Linux 文件系统(~/ 路径下)而不是 /mnt/c/。
相关阅读
- 二进制文件合并冲突 — 手动无法 resolve
- Clone 之后 git hooks 不执行
- 历史里的大文件让 push 失败
- AI package-lock 冲突处理
- Force push 覆盖了队友的 commit
- Codex 没有更新 lockfile
标签: #git #version-control #排查