
说实话这些年带跨平台项目我见过最普遍、也最容易被低估的问题就是Git行尾符。一个Windows同事早上提交了两个文件macOS的同事pull下来终端里刷出几十行同类warning“CRLF will be replaced by LF in assets/xxx.png”。再往后有人git diff看到一整片一整片的红色有人明明没改文件却被git status提示modified还有人部署到Linux服务器后脚本直接报bad interpreter。这些看着毫不相关的怪事八成都是同一个源头CRLF与LF的跨平台之争。这篇文章想系统地把这件事讲清楚包括行尾符的历史和原理、Git三个核心配置到底怎么选、为什么我强烈建议用.gitattributes把规则固化进仓库以及一支Unity跨平台团队从告警刷屏到彻底安静的完整实操记录。适合正在被行尾符折磨的新人也适合想给团队定规矩的负责人。1. 行尾符到底是什么为什么一场“回车”能引发跨平台噩梦1.1 \r与\n从打字机时代继承下来的两个字符要理解CRLF和LF先要回到一个很古老的场景电传打字机。那时候“回车”Carriage Return\rASCII码0x0D和“换行”Line Feed\nASCII码0x0A是两个独立的机械动作。回车是让打印头回到行首换行是让纸张往上走一行。现代系统沿用了这两个字符但继承方式完全不同。Unix和Linux从一开始就只保留了一个换行符LF也就是0x0A。Windows走的是另一条路它继承了MS-DOSMS-DOS又继承了CP/MCP/M则把打字机时代的两个动作原封不动地保留成了两个字符也就是CRLF0x0D 0x0A。早期Mac系统还单独用过CR\r做行尾后来macOS转向Unix内核后才改用LF。这就造成了最直接的冲突同一个文本文件在Windows记事本里看换行是两个字符在Unix工具里看换行是一个字符。两边的工具链对“一行结束”的定义都不一样。你可以把LF理解成“按一下发送键”把CRLF理解成“先回车再发送”。Windows用户习惯了两个动作的合成效果Unix用户只认那个单个动作。1.2 Git为什么非要管这件事Git本质上是一个字节级别的版本控制系统。它做diff、做merge、做patch都以“行”为基本单位而行尾符直接决定了一行在什么时候结束。假如仓库里同一个文本文件有时存CRLF、有时存LFGit看到的就不是“同一行的结尾有点差异”而是“这文件每一行都不见了、又冒出了新的一行”。举个例子你在Windows上把文件保存成CRLF提交入库同事在macOS上pull下来他的工具链默认按LF解析于是每行结尾都多出一个看不见的\r字符。他用编辑器一打开感觉整份文件都被改过了他再保存一次Git又认为整份文件都被改过了。最糟糕的情况发生在脚本和配置文件上Linux的Shell遇到CRLF会把\r当成命令参数的一部分于是报出“/bin/bash^M: bad interpreter”这种让人摸不着头脑的错误。所以Git提出了一个折中方案仓库里统一存放LF格式工作区里Windows用户看到CRLF、macOS和Linux用户看到LF。换句话说提交的时候自动把CRLF翻译成LF检出的时候再按当前平台自动翻译回去。这就是“行尾符转换”的初衷。想法很好但问题在于Git给了很多开关每个开关还受平台和安装选项影响大家配得不一致反而制造出更多的混乱。2. 三个核心配置参数先弄清它们再动手2.1 core.autocrlf的三档取值分别解决什么场景Git处理行尾符的核心开关是core.autocrlf它有三个值true、false、input。理解它其实只需要记住两个时机提交入库时做什么、检出到工作区时做什么。core.autocrlf配置提交时入库检出时工作区典型适用场景trueCRLF转LFLF转CRLFWindows为主或团队所有人都在Windowsfalse原样存储原样检出仓库只在单一平台使用不推荐跨平台团队inputCRLF转LF不做转换保持LFmacOS/Linux为主Windows成员也能配合Windows版Git安装时有一个很经典的选项“Checkout Windows-style, commit Unix-style line endings”选这个对应的就是autocrlftrue“Checkout as-is, commit Unix-style line endings”对应的是autocrlfinput“Checkout as-is, commit as-is”对应的是false。很多新人安装时一路Next根本不知道自己选了哪档等出了问题才回头查。对于纯Windows团队autocrlftrue是最省心的大家编辑、保存、提交都用CRLFGit在入库时统一转成LF各自的工作区仍然是CRLF谁也感知不到转换过程。对于macOS和Linux用户我一般建议直接设成input或者false让工作区保持LF就好。真正出问题的是跨平台团队Windows成员用truemacOS成员用false同一个文件在两边入库后的内容就不一样了所有人开始互相看到莫名其妙的diff。这里还有一个配套参数core.eol它用来显式指定检出时文本文件应该用的行尾符可选lf、crlf、native。当core.autocrlf不是false时core.eol基本要让位给autocrlf的语义所以日常不需要单独设置。我的经验是先把autocrlf这一个参数理解透再谈其他的。2.2 core.safecrlf宁可报错也别让混合行尾进入仓库除了autocrlf另一个被低估的参数是core.safecrlf。它解决的是“行尾转换会不会产生不可逆结果”的问题。假如一个文件里既有CRLF又有LFGit入库时统一转成LF之后检出时想再还原成CRLF就没法保证还原出原始状态了因为原始文件里两种行尾的位置已经丢失。core.safecrlf有三个取值false是默认值不做检查warn表示发现这种情况时输出警告但允许提交true表示发现这种情况直接拒绝提交。我建议跨平台团队至少设置成warn如果项目已经规范干净可以进一步设成true。但这里有个坑要注意safecrlftrue对某些工具生成的文件会特别严格比如Unity在个别场景下生成的YAML文件会混合多种行尾第一次提交时会直接被Git拒绝。我踩过这个坑后做法是先把safecrlf设成warn跑两周一边提交一边观察警告集中出现在哪些文件上逐个清理干净后再切到true。不要一上来就上最严格档位否则新成员入职第一天提交就被拒体验很糟。2.3 为什么只设置本地Git开关终究不够聊到这里很多人会问那我只要让团队成员统一设autocrlftrue不就行了答案是不够。原因有三个。第一本地配置不进仓库。你没法要求每个新同事都记得修改自己的全局配置更没法保证他们装的Git版本、用的GUI工具不会悄悄改掉这些设置。第二很多GUI工具和编辑器的行为会绕过Git配置。比如有人在Windows上用VS Code把文件保存成了LF另一个人用Visual Studio又保存成了CRLFGit的autocrlf只处理“提交”那一下不同的工作区文件状态仍然会带来额外的diff噪音。第三Git仓库里已经存在的错误行尾不会因为本地配置改变而自动修复需要一次显式的规范化。所以真正靠谱的做法是把规则写进仓库本身让每个clone这个仓库的人都自动遵守。这就是.gitattributes。3. 终极解法用.gitattributes把行尾符规则固化进仓库3.1 一份可以直接抄走的.gitattributes模板通用Unity专项.gitattributes是仓库根目录下的一个普通文本文件它的每一行由两部分组成一个文件匹配模式以及一组属性。Git在处理文件时会读取这些属性决定要不要做行尾转换、要不要做diff、是不是二进制文件。最核心的属性有三个text表示“这是文本文件应该做LF规范化”-text表示“不要做任何文本规范化”binary是“-text -diff”的简写既不做行尾转换也不做文本diff。在此基础上可以用eollf或eolcrlf来强制某个匹配模式的文件在检出时使用指定行尾。下面这份模板是我在Unity跨平台项目里使用的版本如果你的是纯代码项目把Unity相关扩展名删掉即可# 兜底规则能被检测为文本的一律做LF规范化 * textauto # 脚本与配置文件明确要求LF避免Shell/CI环境踩坑 *.cs text eollf *.sh text eollf *.json text eollf *.md text eollf *.txt text eollf *.yml text eollf *.yaml text eollf *.xml text eollf # Unity文本资源YAML格式入库统一LFdiff清晰 *.unity text eollf *.prefab text eollf *.anim text eollf *.controller text eollf *.mat text eollf *.meta text eollf # 二进制资源彻底隔离不做任何行尾转换 *.png binary *.jpg binary *.jpeg binary *.psd binary *.tga binary *.tif binary *.wav binary *.mp3 binary *.mp4 binary *.fbx binary *.dll binary *.exe binary *.unity3d binary说明几个细节。第一第一行的* textauto是兜底规则凡是Git能识别为文本的文件都按“入库LF”处理其他没匹配到后面具体规则的文件也不会漏掉。第二后写的具体规则会覆盖前面的兜底规则所以*.png binary这种规则一定要放在* textauto之后否则会被兜底规则抢走。第三eollf表示检出时强制LF这意味着在Windows上checkout出来的这些文件也会是LF格式对于那些希望在Windows上也用LF的团队来说非常合适。关于.asset这类文件我要特别提醒Unity默认序列化文本时.asset是YAML文本但如果你或同事在Project Settings里开启了Force Binary.asset就会变成二进制。所以我不会在通用模板里写死*.asset的规则而是建议你先在某个.asset文件上运行git check-attr text -- file再决定要不要单独加规则。3.2 存量仓库如何批量纠正git add --renormalize的完整流程如果你的仓库已经跑偏很久里面堆满了CRLF入库的文本文件这时候光是放下.gitattributes还不够因为Git只会按新规则处理“接下来提交的文件”历史入库的blob不会自动更新。你需要对Git索引做一次“重规范化”。Git 2.16及以上版本提供了一个专门命令git add --renormalize .。它的作用是用当前仓库的attributes规则重新计算所有已跟踪文件的入库内容但不会修改你工作区里的实际文件。也就是说你本地没提交的修改会完好无损地留在工作区。完整流程是这样# 1. 确认Git版本足够新 git --version # 2. 写好.gitattributes放到仓库根目录 # 3. 先提交.gitattributes本身 git add .gitattributes git commit -m chore: add line ending rules # 4. 用新规则重新计算所有已跟踪文件 git add --renormalize . # 5. 查看这次规范化影响了哪些文件 git status # 6. 确认无误后提交 git commit -m chore: normalize line endings for cross-platform执行完第4步后git status里通常会出现两类文件一类是本来就没遵守LF规范的文本文件它们会显示为modified另一类是之前被误判为文本的二进制文件它们可能也会跟着变化。你需要对照.gitattributes逐一确认尤其注意那些二进制文件如果git diff后看到大片乱码说明文件进了错误的状态赶紧在attributes里补上对应的binary规则重新执行renormalize。如果Git版本太老没有--renormalize备选方案是git rm --cached -r .然后再git add .但这个方案会对所有文件重新计算而且和Git LFS等扩展配合时容易出问题我不推荐。3.3 文本与二进制的边界为什么一个png会被Git当成文本回到文章开头那个告警“CRLF will be replaced by LF in assets/1787552212360.png”。png明明是二进制图片为什么Git会对着它做行尾转换答案藏在Git对textauto的启发式检测里。当某个文件没有显式属性时Git会读取文件内容的前8000个字节左右判断其中是否包含NUL字节0x00。如果检测到NUL就认定这是二进制如果没检测到就可能把它当作文本处理。PNG、JPG这类格式的文件头和部分数据段完全可能在前8000字节里不出现NUL尤其是体积很小的简单图片于是就被Git误判成了文本。一旦它被当成文本git就会尝试做CRLF/LF转换于是那条warning就出来了。更隐蔽的是另一种情况文件本身是二进制但在历史提交中已经被当成了文本并完成了行尾转换图片数据里个别字节变成了0x0D 0x0A。这时候图片可能还能打开但做diff时会整片乱码某些校验严格的场景甚至会直接报“文件损坏”。这也是我在模板里给所有图片格式显式标注binary的原因binary规则不只是为了消除warning更是为了阻止Git用文本逻辑去“修复”二进制文件。遇到png的CRLF告警时第一反应不应该是去修改这个png本身而是去仓库根目录检查.gitattributes里有没有覆盖这个扩展名。填上*.png binary再跑一轮renormalize告警自然会消失。4. 实战复盘一个Unity项目从刷屏告警到彻底安静4.1 现场还原assets/1787552212360.png的告警是怎么来的有次我接手一个Unity跨平台项目Windows开发机和macOS开发机混合使用。某天美术同事把一张新导出的图标丢进assets目录文件名是一个13位时间戳也就是1787552212360.png这种风格一看就是引擎或工具批量生成的。他提交时Git突然刷出一条warningCRLF will be replaced by LF in assets/1787552212360.png。当时项目里的真实情况是仓库没有任何.gitattributes有的成员安装Git时选了autocrlftrue有的选了input还有一位老兄一直是false仓库里已经存在部分CRLF入库的文本文件同时一些很小的png因为启发式判断没检测出NUL被错误的当成文本纳入了行尾转换流程。其他人pull之后不是看到git diff整片乱码就是发现git status永远显示某个文件是modified。问题的根源早就不是某个png而是整个仓库的行尾规则完全失控。4.2 操作实录从改配置到提交一步步做完我处理这个项目时按下面这个顺序操作每一步的命令都可以直接参考。第一步先摸清现状。我建议每个仓库在动手前都跑一遍这几个命令# 查看当前autocrlf配置来源 git config --show-origin --get core.autocrlf # 查看特定文件的入库行尾状态iindexwworktreeattr属性 git ls-files --eol assets/1787552212360.png # 查看这个文件被attributes判定成了什么类型 git check-attr text -- assets/1787552212360.png如果git ls-files --eol输出里那个png文件显示类似i/crlf w/crlf说明这个png在索引里都已经带了CRLF如果git check-attr显示text: auto说明Git把它当成了文本。这两条信息合在一起就完全解释了告警的来源。第二步写好.gitattributes并提交。我直接把3.1节的模板放进去然后执行git add .gitattributes和一次单独的commit。这一步是为了让规则先进入仓库再让其他成员pull时能同步看到。第三步执行规范化git add --renormalize . git status这次git status里出现了几十个文件包括之前被误判的png和被CRLF污染的*.unity、*.cs、*.meta文件。检查下来没有意外我就直接提交了。提交后再次运行git ls-files --eol assets/1787552212360.png这次输出里png已经变成i/lf w/lf attr/binary或类似状态取决于工作区实际内容而*.unity文件显示为i/lf w/lf attr/text eollf说明索引里已经是LF工作区也是LF规则生效了。Windows同事pull后他们的工作区里文本文件会根据autocrlf重新以CRLF检出但因为仓库里已经是LF不会再刷warning。一个值得注意的操作细节renormalize只更新索引不动工作区文件所以理论上不需要stash。但我仍然建议在跑规范化的那个下午先把手上未提交的改动放到一边因为后面检查git status时如果混入你自己的业务改动会很难确认哪些是行尾规范化带来的变化。更稳妥的做法是先把未提交内容提交或stash规范化完成后再恢复。4.3 换行符引发的诡异Bug脚本的“看不见的字符”仓库安静下来之后团队又遇到了一次典型的换行符Bug这次跟Unity无关但很有代表性。项目里有个build.sh在Windows上编辑保存后提交到了仓库。macOS同事本地跑没问题但CI跑起来直接报错/bin/bash^M: bad interpreter: No such file or directory。这个^M就是CR里的\r字符在终端里的显示形式。Linux的Shell把build.sh\r当成了解释器路径自然找不到。排查方法很简单在Linux或macOS上执行file build.sh输出会明确显示with CRLF line terminators。修复方法是用dos2unix build.sh或者直接在编辑器右下角把行尾改成LF然后重新提交。重点是把规则写进.gitattributes也就是模板里的*.sh text eollf这样以后Windows上保存的.sh文件入库时也会被强制转成LF。同样的坑还出现在Makefile上CRLF会让Make报出诡异的missing separator错误看起来像是语法问题实际只是行尾问题。Python脚本、Node.js的bin脚本、npm包里的shell脚本都是重灾区。只要规则里把这些文件统一成LF这些Bug可以一劳永逸地避免。4.4 跨平台开发团队的几条隐性约定经历了那次规范化之后我给团队定了几条简单的约定虽然不是强制工具但效果很好。仓库层面.gitattributes必须存在并且是每次代码评审的必看项。凡是新增了一种文件类型都要顺手确认它在attributes里属于文本还是二进制。Windows成员保持autocrlftruemacOS/Linux成员保持autocrlfinput或false核心是“仓库永远LF”。新成员入职后第一件事不是配IDE而是确认Git安装选项和core.autocrlf再clone一遍项目。编辑器层面VS Code看右下角Visual Studio用“文件 高级保存选项”JetBrains系列看右下角行尾图标。团队约定源代码文件不管在什么平台都统一以LF保存。这样即使某个人临时绕过Git规则也不会把CRLF带进工作区。CI层面如果项目足够正规可以在流水线里加一个简单的行尾检测步骤grep -rl $\r --include*.cs --include*.sh --include*.unity . exit 1 || echo line endings OK这条命令会检测指定类型文件中是否出现CR一旦出现就让CI失败。它能确保规范化成果不被某次临时提交悄悄破坏。5. 常见问题速查这些坑基本都踩过5.1 问题现象与解决对照表我把这些年踩过的行尾符问题整理成了一张速查表遇到对应现象可以直接按里面的思路处理。现象根本原因处理方式git diff显示整个文件全红文件所有行尾整体被替换用git diff --ignore-space-at-eol临时验看用.gitattributesrenormalize统一规则git status一直显示某文件modified但内容没变化index与工作区行尾不一致且attributes缺失git add --renormalize file并补上对应的attributes规则提交时提示“CRLF will be replaced by LF”autocrlftrue且该文件被判定为文本判断该文件类型文本就让它转换二进制就在attributes里加binaryLinux报bad interpreter或bash\r错误脚本文件被CRLF污染dos2unix修复attributes里为*.sh加eollfMakefile报missing separatorMakefile被CRLF污染改成LFattributes里加*.mk text eollf二进制文件diff显示成乱码文本被textauto误判为文本attributes里标记binary并renormalize终端查看文件到处是^M文件是CRLF但工具按LF显示确认是否需清理需清理时用dos2unixstash apply/merge出现无关冲突两个分支的行尾状态不一致统一attributes后重新合并必要时重做规范化5.2 快速自查技巧如何一眼看出是不是行尾符问题判断一个诡异问题到底是不是行尾符引起的我有几个最快的手段。git ls-files --eol file是最直接的它输出文件在索引i/、工作区w/和属性attr/三个维度上的行尾状态。例如看到i/lf w/crlf attr/text eollf就知道索引已经LF化了问题只出在工作区还没同步。看到i/crlf就知道索引里本身就不干净。git check-attr text -- file用来确认attributes规则对这个文件做了什么判断输出text: auto表示走自动检测text: set表示显式文本binary表示纯二进制。在Linux/macOS上file file会直接告诉你文件是否带CRLF。在Windows上则可以用VS Code打开后看右下角或者用Notepad的“视图 显示符号 显示行尾”。如果你想检查整个工作区有没有混入CRLF可以用一条grep命令扫指定类型的文件这也是我在CI里用的方法。命令行还有一个小技巧git diff --ignore-space-at-eol可以忽略行尾差异来做diff如果执行后文件内容瞬间“正常”了那基本可以断定问题就在行尾符。我个人这两年跨平台项目的体会是行尾符问题不存在“忍一忍就好”它越早定规则成本越低。我建议每个仓库第一条提交就顺手带上.gitattributes如果项目已经混乱挑一个改动少的周五下午按4.2节那套流程做一次规范化。最后分享一个小技巧调试时如果怀疑某个文件被行尾符搞了先跑git ls-files --eol file三列状态顶你翻十篇文档。