git 的 NFD 与 NFC 有什么区别?为什么我有个文件在 NFC 中间不会被当成改动,在 NFD 中就会当成改动
这个问题核心在于 Unicode 正规化 和 Git 底层存储机制 的交互。
很多跨平台开发(特别是涉及macOS 和 Windows/Linux 协作)的人都会被这个细节坑过。
1. NFD 与 NFC 的区别
在 Unicode 中,带音符或特殊符号的字符(如 é 或日文假名)有两种编码表达方式:
- NFC(Normalization Form C,预组合): 用一个 Unicode 字符表示。例如 é 直接存为一个 Code Point (U+00E9)。
- NFD(Normalization Form D,分解): 把基础字符和修饰音符拆成两个 Unicode 字符。例如 é 会被拆成 e (U+0065) + 组合音符 ´ (U+0301)。

虽然它们在屏幕上看起来一模一样,但在计算机内存里是完全不同的两个字节序列。
2. 为什么 NFD 会被 Git 当作改动,而 NFC 不会?
这与 Git 的比对逻辑 以及 操作系统文件系统 的特性紧密相关:
- Git 内部是"二进制敏感"的 、本质上只关心字节(Byte)。
- 在 Git 的 Index(暂存区)和 Commit 树结构中:
如果 Git 记录的文件名是 NFC 字节序列,而文件系统传过来的文件名变成了 NFD 字节序列;
- Git 会对比这两个字符串,发现字节完全不同,于是它认为:
旧的 NFC 文件被删除了,同时新建了一个 NFD 文件。
- macOS(APFS / HFS+)的"强制转换"
-
macOS 系统: (如早期 HFS+ 以及 APFS 的某些兼容层)会在你创建文件时,强制将文件名转换为 NFD 格式落盘。
-
Windows / Linux 系统: 默认使用 NFC 格式存储文件名,且不强制转换。
为什么在 NFC 中没有改动,转为 NFD 就报错?
如果你在 Git 仓库中最初提交该文件时,文件名是 NFC 编码:
-
在 NFC 环境下: 你的本地文件名也是 NFC 字节,Git 比对暂存区(NFC)和工作区(NFC),字节一致 → 没有改动。
-
在 NFD 环境下: 文件名变成了 NFD 字节。Git 去比对暂存区(NFC)和工作区(NFD), Git 觉得文件名字变了!
即使你的文件内容一行没改,git status 也会把这个文件标记为已修改或待重命名。
3. 如何彻底解决这个问题?
为了防范跨平台(特别是 Mac 与 非Mac 协作)时的文件名乱象,Git 提供了专门的配置项:
方案一:开启 Git 的 precomposeunicode(针对 Mac 用户)
在 macOS 终端根目本项目运行以下命令:
Bash
git config --local core.precomposeunicode true
原理: 开启后,Git 会在读取 macOS 文件系统时,自动把文件系统的 NFD 文件名转换为 NFC 再交由 Git 内部处理。
这样暂存区和本地工作区就统一为 NFC,解决由于文件名编码导致的伪改动。
方案二:统一将仓库中的文件名重命名为 NFC
如果你的仓库里已经混入了 NFD 编码的文件名,可以通过 Git 强制修复:
Bash
# 查看并让 Git 自动检测格式
git rm --cached -r .
git reset --hard HEAD
(注意:执行前请务必先提交或备份你当前真正的代码修改)
混合开发中必须用 NFC 编码。
不仅你的团队应该用 NFC,整个软件工程界(Web 标准、Linux/Windows 文件系统、各类编程语言的字符串处理)默认的"标准形态"都是 NFC。
- 主流生态的默认标准: Windows、Linux、绝大多数数据库(MySQL、PostgreSQL)以及编程语言(Python, Node.js, Java 等)内部对 Unicode 的处理都是基于 NFC 的。
- 跨平台兼容性: 如果仓库里存了 NFD 文件名,Windows 或 Linux 用户拉取后,极易出现文件名乱码、无法访问文件、或者在 Windows 上产生奇怪的伪改动。
- HTTP 与 URL 安全: 在 Web 开发中,URL 中的中文或特殊字符经 encodeURIComponent 编码后,NFC 和 NFD 会变成完全不同的 %XX 字节串。NFC 能避免路由匹配失效或图片 404 的问题。
macOS 开发者(必须执行)
开启 core.precomposeunicode,让 Git 在读取 macOS 文件系统时,自动把磁盘上的 NFD 转换为标准 NFC:
Bash
git config --global core.precomposeunicode true
Windows / Linux 开发者
Windows 和 Linux 的 Git 默认开启了忽略文件名大小写及兼容配置,保持默认即可:
Bash
git config --global core.ignorecase true