把 .bashrc、vimrc、ssh 的 config 和几个自己写的小脚本塞进同一个仓库,家里台式机和一台小主机共用一套,这个念头我拖了很久才动手。真弄的那天,第一次 clone 到 Windows 上就同时撞了三件事:刷屏的换行符警告、脚本跑到服务器上 Permission denied、还有一个软链接变成了普通文件。下面五条是按踩坑顺序记的,环境是 git 2.55.0.windows.3 加 Windows 11。
一、先把 LF will be replaced by CRLF 这句话读懂
第一次 add 一个用 LF 存的文件,警告原文是这样,一行一条:
warning: in the working copy of 'note.txt', LF will be replaced by CRLF the next time Git touches it
很多人看到这句以为文件被改了,其实不是。core.autocrlf=true 干的事是:提交时把工作区的 CRLF 折成 LF 存进库,检出时再把 LF 摊回 CRLF。所以库里永远是 LF,工作区永远是 CRLF,警告只是在说"下次碰到这个文件会转换一次",不丢内容,也不用去 diff。
有人图省事,直接 git config --global core.autocrlf false 把警告关掉,我不建议。关掉之后工作区什么样就什么样进库,一个仓库里会混着 CRLF 和 LF 两种行尾,下次谁只改了一行,diff 里整篇都是改动,比警告难看得多。留着警告,用规则把它局部关掉,成本更低。
要确认库内和工作区到底是哪一种,别靠肉眼,用这条:
git ls-files --eol note.txt
i/lf w/crlf attr/
i 是 index,也就是库内;w 是 worktree,也就是盘上那个文件。我这行读出来是库内 LF、盘上 CRLF,转换方向一目了然。attr 那栏空着,说明这个文件没有命中任何 .gitattributes 规则,全靠全局配置在起作用——这一点后面还会再提。
二、脚本提交上去是 100644,到服务器上就是 Permission denied
Windows 没有可执行位这个概念。在 Git Bash 里 chmod +x run.sh,ls -l 确实显示 -rwxr-xr-x,看着像成功了。但 git 记的是另一回事:
git ls-files -s run.sh
100644 4163036efa65bd4a469e752267498f01ea36a55c 0 run.sh
还是 100644。原因是 git config core.filemode 在这台机器上返回 false,git 默认不去读那个位。于是在 Windows 上 chmod 完再提交,模式压根没进仓库,脚本跑到小主机上就是 bash: ./run.sh: Permission denied,而本地一切正常,最难查。
顺便说清楚为什么 ls -l 那个 rwx 不算数:Git Bash 的 chmod 只是记在 MSYS 自己的映射里,NTFS 的权限模型没有可执行位这个说法,git 要不要去读它,全看 core.filemode 这个开关。所以同一份仓库拷到 Linux 上,那个位立刻就有意义,问题只出在 Windows 这一侧。
补救只有一条路,直接改索引里的模式:
git update-index --chmod=+x run.sh
git ls-files -s run.sh
100755 4163036efa65bd4a469e752267498f01ea36a55c 0 run.sh
注意前后 blob 的 hash 一模一样,说明只动了模式、没重写内容,这也是为什么它能立刻生效、不用重新 add。改完要单独提交一次,只在工作区改是留不住的。仓库里这类文件多了,可以拿 git ls-files -s 全列出来扫一遍 100644,看着像脚本的都补一下。
三、中文文件名在 git status 里变成一串 \344\270\255
装完第一件事一般是 git status,中文文件名会显示成这种:
A "\344\270\255\346\226\207\351\205\215\347\275\256.conf"
文件本身没坏,是 core.quotepath 默认开着,把非 ASCII 字符按八进制转义了,好处是终端编码不一致时也不至于糊成一片。自己用着嫌烦可以关掉:
git config --global core.quotepath false
关掉之后同一个文件显示为 中文配置.conf。不想动全局配置也行,临时看一眼就加 -c:git -c core.quotepath=false status,只对这一次命令生效。顺带说一句,commit message 里的中文不受这个开关影响,它只管 status、diff、log 里的路径怎么显示。
四、软链接会悄悄变成一个 8 字节的文本文件
这条最阴。Git Bash 里 ln -s note.txt link.txt 是能建出来的,ls -l 也有箭头,索引里记的确实是符号链接:
git ls-files -s link.txt
120000 97922aee98474ad751b77adb4aa5e4f3dce681fe 0 link.txt
120000 就是符号链接的模式。但这台机器的 core.symlinks 是 false。换一台 Windows 机器 clone 下来,link.txt 变成一个 8 字节的普通文件,cat 出来就一行字:
note.txt
读到的是路径字符串本身,不是内容。更麻烦的是 clone 完 git status 是干净的,不会有任何提示,等哪份配置去 source 它的时候才炸,而且报错通常指向别处。所以共享配置的仓库里别用软链接串文件,宁可每个文件单独存在,或者在配置里写一句 source 加绝对路径。
真要用链接也不是没办法:Windows 设置里打开开发者模式,把这个仓库的 core.symlinks 改成 true,再把已经落成文本的那几个文件删掉重新检出,才会变成真链接。直接在那个文本文件上改是没用的,它已经不是链接了。
五、规则写进 .gitattributes,比让每个人改全局配置靠谱
core.autocrlf 每台机器可能都不一样,我这台 git config --global core.autocrlf 直接是空的,等于没设。指望每台机器都去配一遍不现实,把规则写进仓库里的 .gitattributes 更稳,它跟着仓库走:
*.sh text eol=lf
.gitattributes text eol=lf
加完这两行,再 add 一个 .sh 就没有 LF 转 CRLF 的警告了。规则存进仓库立即生效,不用重启什么,也不跟在不在原来的机器上有关,别人 clone 下去拿到的是同一套。第一行只管 *.sh,.gitattributes 自己不在里面,所以它照旧会警告一次,第二行就是专门给它补的。想确认某个文件最终命中了哪条规则,用 git check-attr -a s2.sh,输出是 text: set 和 eol: lf 两行;拿没命中的文件去问,什么都没有,空输出也算答案,比翻文档猜快。
同一种写法还能按类型分开管,比如 *.bat text eol=crlf。批处理必须是 CRLF,用 LF 存下来,cmd 解析 goto 和标签时会跳错行,这种问题在共享仓库里特别容易被别人顺手带进来,事后查起来又完全看不出跟换行有关。
你们那套配置是直接扔在仓库里,还是用 stow 一类的工具在本地生成链接?我这边软链接这条路算是放弃了,还想听听有没有更省事的做法。