你在仓库里加了 .gitignore 规则,不让某些「本地专属」的文件入库——本地配置、私有的笔记、临时的机器说明。文件确实没进仓库,可推送之后,别人打开 README,发现里面指向这些文件的链接点不开,全是死链。
现象
.gitignore规则生效了,那些本地专属文件确实没被提交;- 但 README(或其它被跟踪的文档)里仍然写着指向它们的引用,比如「细节见
local-setup.md」「配置参考deploy-local.md」; - 别人 clone 下来,点这些链接,404——文件在远端根本不存在;
- 你自己本地没事,因为文件还在你机器上,链接「看起来是通的」。
最迷惑的是:在你本地一切正常,只有拉取你代码的人才会遇到死链。所以问题往往要过一阵、等别人反馈才知道。
根因
根因是一句需要点破的区分:
.gitignore只决定「什么不进版本库」,它不会去改动已经写进其它文件的内容。
具体地说:
.gitignore的作用是「告诉 git,这些路径不要跟踪」。它只影响提交行为。- 而 README 是一个被跟踪的文件,它里面的链接、引用是普通文本内容。
- 你排除一个文件出版本库时,没有任何机制会去检查「有没有别的地方引用了它」,更不会自动删除那些引用。
于是形成一个矛盾状态:被引用的目标不存在了,引用的文字还在。 在 git 看来,README 是被正常提交的、内容合法的文件;它不知道也不关心里面那个链接指向的东西已经被你排除掉了。
这就是「排除一个东西」和「排除对它的引用」是两件独立的事。 你只做了前一件,忘了后一件。
解决
新增本地专属文件时,顺手检查一遍被跟踪文档里有没有引用它。
改动的方向,按情况选:
一、如果那个引用不再需要——删掉它。
把 README 里指向本地专属文件的句子或链接删掉,让文档只描述「仓库里确实存在的东西」。
二、如果只是想说「细节见别处」——改成不点名的说法。
不要写「细节见 local-setup.md」,改成不指向具体文件名的表达:
本地开发环境的细节请参见你的私有笔记,本仓库不包含这部分内容。
关键是不点名——不出现那个不存在于仓库里的文件路径。这样即使文件在远端不存在,文档也是自洽的。
三、如果这个引用对读者有用——把目标也纳入版本库。
反过来想:如果 README 里提到它是为了让读者能看到细节,那它也许本来就该被跟踪,而不是被忽略。重新判断这个文件到底是「本地私有」还是「项目的一部分」。
改完之后,验证一遍:在干净的 clone 里检查有没有死链。 或者简单地用工具搜一下,所有被跟踪文档里的引用路径,能不能在被跟踪的文件列表里找到。
延伸与预防
一句话原则:
把文件排除出版本库,不等于把对它的引用也排除了。
更通用的认知是:「实体」和「对实体的引用」是两个独立的东西,它们的一致性需要你显式维护。
这个模式在很多地方重现:
- 删了一个函数,但注释里还写着它;
- 改了配置项的名字,文档里还是旧的;
- 下线了一个接口,调用方的文档没更新;
- 重命名了文件,别处的路径引用没跟着改。
所有这些的共同点是:删除/排除/重命名一个东西时,「找到所有引用它的地方」这一步经常被漏掉。 而对本地专属文件来说,这一步尤其容易漏——因为在你的本地环境里,它没坏,你感受不到问题。
一个可操作的习惯:做「排除、删除、重命名」这类操作时,把「全局搜索旧名字」当作固定的收尾动作。 就像批量替换之后要「全局搜一遍确认零命中」(见本系列另一篇)一样——删除之后,也要「全局搜一遍确认没有残留引用」。
具体到 .gitignore 这个场景,还有一个更根本的建议:在写 README 的时候就避免引用本地专属文件。 文档是给「clone 到你仓库的人」看的,不是给「你这台机器」看的。写文档时脑子里要有一个「干净的仓库」的视角——你的文档里出现的每一个路径,都应该能在仓库里找到。