Git 换行符 — 终结 Mac/Linux 与 Windows 之间的 CRLF 混乱
一个 .gitattributes 文件即可保证所有地方使用相同的换行符。core.autocrlf 的陷阱与正确做法。
Mac/Linux 与 Windows 共用同一仓库时,几乎必然会遇到这个问题——CRLF vs LF。PR diff 炸开成「明明什么都没改,却有 1000 行变更」,shell 脚本报 \r: command not found,Python 抛 SyntaxError: invalid character。
我认为换行符问题的根本不在于 Windows 的历史遗留,而在于团队没有在 .gitattributes 中明确声明意图。以前靠 core.autocrlf 的个人设置;如今通过仓库级的 .gitattributes 声明,不论操作者用什么系统,换行符行为都一致,因为配置活在代码库而不是个人机器上。
本指南介绍如何通过 .gitattributes 在所有操作系统上统一换行符的正确方法。配置一次,永不复顾。
TL;DR
- 在仓库根目录添加
.gitattributes,写入* text=auto eol=lf - 关闭
core.autocrlf—.gitattributes会覆盖它 - 对 Windows shell 脚本强制 LF(
*.sh text eol=lf) - 对 Windows 批处理文件强制 CRLF(
*.bat text eol=crlf) - 统一后,运行
git add --renormalize .重新对齐现有文件
前置条件
- Git 2.10+(支持
.gitattributes中的eol=) - 仓库被 Mac/Win 用户同时使用
1. 问题根源 — CRLF 为何挥之不去
1.1 各 OS 默认换行符
| 系统 | 换行符 | 历史 |
|---|---|---|
| Unix · macOS · Linux | LF(\n,0x0A) | Unix 1970+ |
| Windows | CRLF(\r\n,0x0D 0x0A) | DOS 1980+ |
| 经典 Mac OS 9 | CR(\r) | 1984–2001 |
1.2 Git 的自动转换 — core.autocrlf
| 值 | 检出时 | 提交时 |
|---|---|---|
true(Windows 默认) | LF → CRLF | CRLF → LF |
input(Unix 推荐) | 不转换 | CRLF → LF |
false | 不转换 | 不转换 |
问题在于:每个用户的设置不同,新机器的默认值各异,隐式转换会导致「我没改任何东西,但它变了」这类事故。
1.3 正确做法是使用 .gitattributes
将 .gitattributes 放入仓库,对所有人强制统一规则,并覆盖 core.autocrlf。
2. .gitattributes 基础配置 — 5 分钟
仓库根目录的 .gitattributes:
# 默认 — 将所有文本文件存储为 LF;自动检测文本文件
* text=auto eol=lf
# 显式文本文件 — 强制 LF
*.c text eol=lf
*.cc text eol=lf
*.cpp text eol=lf
*.h text eol=lf
*.hpp text eol=lf
*.cs text eol=lf
*.go text eol=lf
*.java text eol=lf
*.kt text eol=lf
*.py text eol=lf
*.rb text eol=lf
*.rs text eol=lf
*.swift text eol=lf
*.ts text eol=lf
*.tsx text eol=lf
*.js text eol=lf
*.jsx text eol=lf
*.mjs text eol=lf
*.cjs text eol=lf
*.json text eol=lf
*.jsonc text eol=lf
*.yml text eol=lf
*.yaml text eol=lf
*.toml text eol=lf
*.md text eol=lf
*.mdx text eol=lf
*.txt text eol=lf
*.html text eol=lf
*.css text eol=lf
*.scss text eol=lf
*.svg text eol=lf
*.xml text eol=lf
*.sql text eol=lf
# Shell 脚本 — LF(CRLF 会导致脚本损坏)
*.sh text eol=lf
*.bash text eol=lf
*.zsh text eol=lf
*.fish text eol=lf
Dockerfile* text eol=lf
Makefile* text eol=lf
# 仅限 Windows — CRLF
*.bat text eol=crlf
*.cmd text eol=crlf
*.ps1 text eol=crlf
*.psm1 text eol=crlf
# 二进制文件 — 不转换
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.ico binary
*.webp binary
*.avif binary
*.pdf binary
*.zip binary
*.gz binary
*.tar binary
*.7z binary
*.exe binary
*.dll binary
*.so binary
*.dylib binary
*.woff binary
*.woff2 binary
*.ttf binary
*.otf binary
*.mp3 binary
*.mp4 binary
*.mov binary
# 静默 diff — 文本但无需 diff
*.lock text eol=lf -diff
pnpm-lock.yaml text eol=lf -diff
package-lock.json text eol=lf -diff
yarn.lock text eol=lf -diff
Cargo.lock text eol=lf -diff⚠️ PowerShell 文件(
*.ps1)需要 CRLF。Windows PowerShell 偶尔会拒绝仅含 LF 的 ps1 文件。
3. 应用到现有仓库 — 5 分钟
添加 .gitattributes 并提交;然后重新对齐已有文件中错误的换行符。
3.1 renormalize
# 在仓库根目录执行
git add .gitattributes
git commit -m "feat: add .gitattributes baseline"
# 按 .gitattributes 重新对齐所有文件
git add --renormalize .
git status # 查看变更
git commit -m "chore: renormalize line endings"--renormalize 不修改工作区,只重写 index。其他人拉取后会自动对齐。
3.2 大型仓库 / 活跃协作
--renormalize 会产生一个巨大的 PR。建议提前协调:
- 设定时间窗口(「X 点之后暂停推送」)
- 合并 renormalize 提交后,让所有人执行一次
git pull - 或在特性分支上执行
git rebase origin/main来吸收变更
3.3 小型仓库 / 个人项目
合并为一个 PR 即可。
4. 用户全局设置 — 驯服 core.autocrlf — 1 分钟
.gitattributes 使 core.autocrlf 在该仓库中无关紧要。但对于没有 .gitattributes 的旧仓库/外部仓库,它仍然有影响。建议:
# macOS / Linux
git config --global core.autocrlf input
# 检出时不转换;提交时 CRLF → LF
# Windows
git config --global core.autocrlf input
# 即使在 Windows 上,也优先使用 input — 你提交的文件始终是 LF。别人的 CRLF 同样会被规范化。
# 或仅依赖 .gitattributes
git config --global core.autocrlf false为什么在 Windows 上也用 input:不受 OS 默认值影响,始终输出 LF。
core.eol
git config --global core.eol lf检出时的默认换行符。.gitattributes 中的 eol= 优先级更高。
5. 编辑器设置 — 对齐 IDE
VS Code(.vscode/settings.json 或用户设置):
{
"files.eol": "\n",
"files.insertFinalNewline": true,
"files.trimTrailingWhitespace": true
}JetBrains:Settings → Editor → Code Style → Line separator → Unix and macOS (\n)。
Vim:
" ~/.vimrc
set fileformats=unix,dos
set fileformat=unix6. 验证 — 换行符是否真的是 LF?
6.1 单个文件
# macOS/Linux
file scripts/build.sh
# ASCII text ← OK
file scripts/build.sh | grep -q CRLF && echo has CRLF
# 或
od -c scripts/build.sh | head -3
# 行尾仅有 \n。如果看到 \r \n,则存在 CRLF。6.2 整个仓库
# 查找含 CRLF 的文件
git grep --cached -lI $'\r' -- ':!*.bat' ':!*.cmd' ':!*.ps1'-I 排除二进制文件。排除已知使用 CRLF 的文件(ps1、bat)。
6.3 添加 CI 门禁
GitHub Actions:
- name: Check no CRLF in LF files
run: |
crlf=$(git grep --cached -lI $'\r' -- ':!*.bat' ':!*.cmd' ':!*.ps1' || true)
if [ -n "$crlf" ]; then
echo "CRLF found in LF-expected files:"
echo "$crlf"
exit 1
fi7. 常见问题与解决方法
7.1 .sh 在 Windows 上报 ^M: command not found
提交了含 CRLF 的 shell 脚本。快速修复:
dos2unix scripts/build.sh
# 或
sed -i 's/\r$//' scripts/build.sh根本修复:在 .gitattributes 中添加 *.sh text eol=lf + 执行 git add --renormalize。
7.2 Python SyntaxError: invalid character
.py 文件含 CRLF + UTF-8 BOM。在 .gitattributes 中强制 LF + 在编辑器中移除 BOM。
sed -i '1s/^\xEF\xBB\xBF//' file.py7.3 .gitattributes 未实际生效
.gitattributes不在根目录而在子目录 — 仅对该子目录生效- 文件未经 renormalize — 现有换行符保持不变(必须执行一次 renormalize)
7.4 IDE 重新保存时转为 CRLF
添加 .editorconfig:
# .editorconfig(位于仓库根目录)
root = true
[*]
end_of_line = lf
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.{bat,cmd,ps1}]
end_of_line = crlfVS Code、JetBrains、Vim、Sublime 等均会自动读取 .editorconfig。
7.5 Windows 编辑器自动添加 BOM
PowerShell Out-File 默认输出 UTF-16 BOM。强制使用 UTF-8:
$content | Out-File "file.txt" -Encoding utf8 -NoNewline
# 或 PowerShell 7+
$content | Out-File "file.txt" -Encoding utf8NoBOM8. 修复现有仓库的 PR 检查清单
- 添加
.gitattributes基础配置(第 2 节) - 添加
.editorconfig(第 7.4 节) -
git add --renormalize .+ 提交 - 添加 CI CRLF 检查(第 6.3 节)
- 通知团队 — 「拉取后,确认
git config --global core.autocrlf input」 - README 中加一行说明 — 「本仓库统一使用 LF,参见
.gitattributes」
下一步
- 多系统文件同步 — 超出 Git 范围的文件同步
- Mac 初始配置 — git 全局基础设置
- Windows 初始配置 — WSL2 git 配置
参考资料
更新日志
- 2026-05-12 — 初稿(devAlice M2 seed 扩充)