文件属性
.gitattributes 是 git 里价值最高、却几乎没人写的那个文件。它是仓库向 git 讲解自己内容的方式:哪些文件是二进制的,哪些应该拼接而不是冲突,哪些永远不该出现在归档里,所有人拿到的行尾是什么。
要紧的一点:它是被提交的。你加的一条规则,会为每一个克隆它的人、在每一种操作系统上、永远地把问题解决掉——不像你自己配置里的一个设置,那只解决了你的问题,然后让同事们自己去碰壁发现。
⌘K → 文件属性。

这些规则做什么
| 属性 | 解决什么 |
|---|---|
text=auto eol=lf |
行尾会随着谁检出这个文件而翻来覆去 |
binary |
git 试图去对比或三方合并一个 PSD、一个 DOCX、一份编译产物 |
merge=union |
一份人人都往后追加、于是人人都冲突的变更日志 |
-merge |
三方合并只会产出胡话的文件——锁文件、生成的代码 |
export-ignore |
被塞进发布包里的 CI 配置和测试夹具 |
diff=<driver> |
那些本来可读、只要有个转换器就可读的格式,差异却没法看 |
filter=lfs |
通过 LFS 存放的大文件 |
linguist-vendored |
在语言统计里被算作你写的第三方代码 |
binary 是 -diff -merge -text 的简写,一个词里装下了对「别再猜这个文件了」的三个回答。
编辑
预设会替你填好一个模式和它的属性;添加之前先改模式——CHANGELOG.md 只是个建议,不是关于你项目的定论。
**改动是外科式的。**为一个已经有规则的模式再加规则,会就地重写那一行,而不是追加第二条、靠位置靠后取胜。文件里的注释原封不动地活下来,因为规则旁边的那个「为什么」,通常比规则本身更值钱。
每一次保存都是一次普通的 Gitcito 操作:会弹提示,而撤销会把文件一字不差地恢复原样。
**一个仓库可以有好几个属性文件。**根目录一个,任何子目录里也可以有,还有一个私有的 .git/info/attributes——它从不被提交,只在你这台机器上生效,正是安放「关于你、而不关乎项目」的那类规则的地方。Gitcito 会把它们全部列出来,并说明哪个是哪个。
某个路径上生效的是什么?
规则来自好几个文件,更具体的那条胜出,靠通读它们来推断结果基本是猜谜。**某个路径上生效的是什么?**会运行 git check-attr,把 git 自己得出的结论摆给你看——那是唯一算数的答案。
差异驱动:让 Word 文档变得可读
一个 .docx 是个 zip。一个 .pdf 是一张压缩过的对象图。git 就按它们本来的样子去对比——也就是噪声——于是文档明明是可读的,它的历史却没法读。
差异驱动用 textconv 解决这件事:一条把文件变成文本、仅用于对比的命令。你工作区里的文件毫发无损,git 只是去比较转换后的文本。
两半,缺一不可:
- git config 里的
diff.<name>.textconv——转换器命令。 .gitattributes里的*.docx diff=<name>——它适用于哪些文件。
这里的按钮一次把两件都做了。对 Word、Excel、JSON 和 .strings,Gitcito 自己附带了转换器——就是它的预览所用的那套文档解析,以应用内一条小小的 gitcito-textconv 命令的形式提供——所以这四个什么都不用装就能用。其余的仍然需要你 PATH 里有真正的工具:Gitcito 会检查,并把缺的置灰,而不是写下一条在第一次对比时就失败的驱动。
| 驱动 | 需要 | 给你什么 |
|---|---|---|
word |
无——随 Gitcito 附带 | .docx 的散文级差异 |
excel |
无——随 Gitcito 附带 | .xlsx/.xls 的按行差异(每个工作表一份 CSV) |
json |
无——随 Gitcito 附带 | 键已排序、结果稳定的 JSON 差异 |
strings |
无需安装——随 Gitcito 附带 | 让 git 视为二进制的 UTF-16 .strings 显示逐行差异 |
pdf |
pdftotext(poppler) |
.pdf 的文本差异 |
exif |
exiftool |
当像素本身看不出名堂时,一张图片改了什么 |
真正咬到 iOS 项目的那个
Localizable.strings 在 Xcode 的大半历史里都是 UTF-16,而 UTF-16 满是 NUL
字节,所以 git 把它当二进制,什么也不显示:
diff --git a/Localizable.strings b/Localizable.strings
Binary files a/Localizable.strings and b/Localizable.strings differ
而这恰恰是最需要看清谁动了哪条文案的文件。strings 驱动只为比较而解码——它读
字节序标记而不是靠猜,所以现代的 UTF-8 .strings 会原样通过,不会变成乱码。
String Catalog(.xcstrings,Xcode 15 起)是 JSON,由 json 驱动覆盖:它会
排序键,于是在顶部新增一条翻译不再让整份文件在 diff 里被重写一遍。
附带的转换器的限制,明说:.doc(老式二进制 Word 格式)不认识,只认 .docx;PDF 没有覆盖——Gitcito 是用浏览器的查看器来预览 PDF 的,没有可以复用的文本提取器;另外每对比一次文档都要付出一小段转换器启动的开销。设置 git config diff.<name>.cachetextconv true 可以让 git 按 blob 缓存输出。
转换器那一半住在你的配置里,而不在仓库里——git 不会去运行一次克隆递给你的命令,这是一条值得守住的安全性质。附带的驱动还指向的是你的 Gitcito 安装路径,所以一个克隆下来的同事拿到的是 diff=word 这条规则,而在他接好自己的转换器(Gitcito 或别的)之前,看到的还是那份没法读的旧差异。在你的 README 里说一声。
clean/smudge 过滤器——先做一次试运行
过滤器会在内容进出仓库的路上重写它:clean 在暂存时运行(工作区 → 仓库),smudge 在检出时运行(仓库 → 工作区)。git-lfs 就是靠它工作的,团队从提交内容里剥掉凭据或生成的噪声,也是靠它。
它同时也是 .gitattributes 能指向的最危险的东西:过滤器会在每一个匹配文件的每一次检出时运行,一个写错的过滤器会悄无声息地毁掉你的工作区。所以 Gitcito 拒绝在这里只当一个文本框。配置一个过滤器,要先对你仓库里真实匹配的文件做一次试运行:
clean命令在每个匹配文件(最多五个)的副本上运行——仓库和它的配置都毫发无损。- 如果给出了
smudge命令,它会在clean过的输出上运行,结果与原文件逐字节比较——这就是往返校验。一个往返不回来的过滤器,意味着检出时还原不出你原来的东西。 - 只有对你正要保存的那组值做过试运行之后,保存按钮才会解锁。失败的试运行——命令报错、没有匹配、或往返结果不一致——仍然可以保存,但只能穿过一条把可能丢失什么讲明白的显式警告。
保存会把 filter.<name>.clean/smudge 写进你本地的 git 配置,把 filter=<name> 规则写进属性文件,并留下一条撤销记录,能恢复配置之前保存的内容。required 开关会设置 filter.<name>.required,让 git 在过滤器坏掉时直接让操作失败,而不是悄悄把文件原样放行。
限制,明说:试运行最多抽取五个匹配文件,每个不超过 5 MB,每条命令有 10 秒超时——在样本上表现良好的过滤器,仍可能在样本漏掉的某个文件上出岔子。命令住在你的配置里,所以克隆的同事拿到的是 filter=<name> 规则,却拿不到命令;没有命令(也没有 required)时,他们的文件会原封不动地通过。
值得知道的限制
text=auto会改变被提交的内容,在写入时把行尾规范化。在一个已有的仓库上,加完之后要有意识地运行一次git add --renormalize .,单独提交这一次。- **属性不会追溯生效。**今天把一个文件标成
binary,不会改变它过去的差异是怎么存下来的;它改变的是从现在起 git 怎么对待它。 - 规则只在文件可见的地方生效。
design/.gitattributes里的规则对src/一言不发。 - Gitcito 是整文件写入的,所以一个手工排版过的文件回来时排版还在——但被 Gitcito 重写过的那条规则,会按 git 规范的
pattern attr attr间距重新排版。
另请参阅:LFS 与稀疏检出 · Bundle 与归档 · 合并选项 · 钩子