Git LFS 系统学习文档

把二进制大文件从 git 历史里搬出去的标准方案。本文按「一步一步能跟着敲」组织, 所有带「实测」标记的命令输出,都是 2026-08-06 在本机真实跑出来的,不是凭记忆写的。 环境:Darwin 25.5.0 (arm64) · git 2.50.1 (Apple Git-155) · git-lfs 3.7.1 实验目录:/tmp/lfs-lab/(用完可直接 rm -rf,全程未触碰任何真实仓库)

⚠️ 本文的诚实边界:有若干条依赖联网/服务端的内容我没验证,完整清单见文末「验证状态」表 (不在这里列举,避免列漏——第一版就漏列了两项)。正文里每条未验证的内容都在原地标了 ⚠️。

📌 同目录相关文档260806-1-如何用Claude以10倍速度学习任何东西_整理版.md ——本文第 13 章的自测题形式(一次只问一道)来自那份笔记里的「方法③ AI 考官」。


目录

  1. 它解决什么问题(心智模型)· 什么时候不该用
  2. 环境确认:你现在处在哪一步
  3. 第一步:跑通最小闭环
  4. 指针文件到底长什么样
  5. .gitattributes:规则写在哪(以及另外两处配置)
  6. 【核心事故】track 只管未来,不管过去
  7. 修历史:git lfs migrate 三步法 · 反向退出 LFS
  8. 克隆端发生了什么:smudge / skip / pull
  9. 空间与配额治理(配额部分对你不适用
  10. Unity 实战:你的仓库体检(实读 + 3 个缺口 + 大小写跨平台陷阱
  11. 反模式清单
  12. 命令速查表
  13. 自测题(不看答案能答出几道)
  14. 学习路径:30 分钟 / 2 小时

1. 它解决什么问题(心智模型)

问题的根源

git 存的是每个版本的完整快照。文本文件可以做行级增量压缩,但二进制文件(psd/fbx/png/wav/mp4)改一个字节,git 就得再存一份全量

一张 50MB 的 psd,改了 20 版
→ git 仓库里躺着 20 × 50MB = 1GB
→ 而且每个 clone 的人都要下载这 1GB
→ 哪怕他只需要最新那一版

LFS 的做法

                 git 仓库(轻)              LFS 服务端(重)
                 ┌──────────────┐           ┌──────────────┐
art.psd  ──────► │ 指针文件      │  ───────► │ 真实二进制    │
(你看到的)      │ 132 字节      │           │ 5 MB × N 版本 │
                 └──────────────┘           └──────────────┘
                        ▲                          │
                        │  checkout 时 smudge 过滤器把真文件换回来
                        └──────────────────────────┘

一句话:git 里只留一个 132 字节的指针,真文件放到独立的 LFS 服务端;你 checkout 哪个版本,就只下载哪个版本。

最容易误解的一点

LFS 不是压缩,不是加速下载。

它不减少你「当前需要的那份文件」的体积,它减少的是历史包袱——你不再需要下载这个文件的全部 20 个历史版本,只下当前这一版。

这个心智模型是后面所有操作的地基。第 6 章那个事故,本质就是没理解这一点。

什么时候不该用 LFS

同样重要的一面,很多教程不讲:

情况 为什么不该
小文件(几十 KB 的图标、配置) 每个 LFS 文件都多一次网络往返和一条元数据,收益为负
纯文本(代码、json、yaml、.meta git 对文本的增量压缩非常好,进 LFS 反而失去 diff/merge 能力
改动极少的大文件 只提交过一两次的话,历史包袱本来就不存在
需要逐行 diff / 合并的文件 进了 LFS 就只能整文件替换

判据不是「文件大不大」,是「它会不会被反复修改」。 一个 500MB 但只提交一次的安装包, 危害小于一个 5MB 但改了 200 版的贴图。


2. 环境确认:你现在处在哪一步

第 1 步:装没装

git lfs version

实测输出:

git-lfs/3.7.1 (GitHub; darwin arm64; go 1.25.3)

没装的话:brew install git-lfs

第 2 步:全局钩子有没有装上

git config --global --get-regexp 'filter.lfs'

实测输出(说明已经 git lfs install 过了):

filter.lfs.required true
filter.lfs.clean git-lfs clean -- %f
filter.lfs.smudge git-lfs smudge -- %f
filter.lfs.process git-lfs filter-process

这四行就是 LFS 的全部魔法所在:

配置 作用 什么时候跑
clean 真文件 → 指针 git add
smudge 指针 → 真文件 checkout/clone
required true 过滤器失败就中止,不静默放行 始终

如果这四行不存在,跑一次 git lfs install(每台机器一次,全局生效)。


3. 第一步:跑通最小闭环

完整跑一遍,5 分钟。建议你现在就照着敲一遍。

mkdir -p /tmp/lfs-lab/demo1 && cd /tmp/lfs-lab/demo1
git init -b main
git config user.name "LFS Lab"
git config user.email "lab@example.com"

① 仓库级初始化

git lfs install

实测输出:

Updated Git hooks.
Git LFS initialized.

② 声明哪些文件走 LFS

git lfs track "*.bin"

实测输出:

Tracking "*.bin"

⚠️ 引号不能省。写成 git lfs track *.bin 的话,shell 会先把它展开成当前目录下的具体文件名, 于是 .gitattributes 里被写进一堆死文件名,新文件不生效。

③ 看看它写了什么

cat .gitattributes

实测输出:

*.bin filter=lfs diff=lfs merge=lfs -text

④ 造个 5MB 文件提交

dd if=/dev/urandom of=big.bin bs=1m count=5
git add .gitattributes big.bin      # ← .gitattributes 必须一起提交!
git commit -m "add big.bin via lfs"

⑤ 验证:git 里实际存的是什么

git cat-file -p HEAD:big.bin

实测输出——这就是指针文件

version https://git-lfs.github.com/spec/v1
oid sha256:f738a438f8a71a513cf5278e1809c1eb31565b5e5108e139177e084268459643
size 5242880

⑥ 诊断命令(出问题时的第一反应)

git lfs ls-files

实测输出:

f738a438f8 * big.bin

🎯 过关标准git cat-file -p HEAD:文件名 看到的是上面那三行文本,而不是二进制乱码。 看到乱码 = 这个文件没有走 LFS。


4. 指针文件到底长什么样

实测:132 字节,3 行

version https://git-lfs.github.com/spec/v1     ← 规范版本,固定
oid sha256:f738a438f8a71a...                    ← 真文件的 sha256,LFS 靠它去服务端取
size 5242880                                    ← 真文件字节数

为什么要知道这个:因为你迟早会遇到这一幕——

同事:「你发我的图打不开,用记事本打开是三行英文。」

那三行就是它。原因见第 8 章。


5. .gitattributes:规则写在哪

LFS 没有隐藏的数据库,「哪些文件走 LFS」这条规则全部写在 .gitattributes 这个纯文本文件里

📌 但 LFS 的配置不止这一处,别记成「只有一个文件」:

  • .gitattributes —— 哪些文件走 LFS(本章内容,会随仓库分发给所有人)
  • .lfsconfig —— 传到哪个服务端(可选文件,同样会分发)
  • .git/config 里的 lfs.* —— 本机凭据/endpoint 覆盖(不分发,只影响你自己)

排查「为什么我这儿正常他那儿不行」时,三处都要看。 (实测:你的 Unity 仓库没有 .lfsconfig,endpoint 记在本机 .git/config 里——见第 10 章)

*.psd  filter=lfs diff=lfs merge=lfs -text
字段 含义
filter=lfs 走 clean/smudge 过滤器(核心)
diff=lfs git diff 时不要吐一屏二进制乱码
merge=lfs 合并时不要尝试按行合并
-text 明确声明是二进制,禁止换行符转换(Windows 队友的救命符

三条必须知道的规则

  1. 它必须被提交并推送。它只是个普通文件,你不提交,别人就没有这套规则。
  2. 它可以放在子目录里,只对该目录生效——大项目里常见 Assets/.gitattributes
  3. 改它不会动已有文件。这是第 6 章的全部内容。

6. 【核心事故】track 只管未来,不管过去

我认为这是 LFS 最高频的翻车点(凭经验判断,没有数据支撑)。下面是实测复现

事故复现

mkdir -p /tmp/lfs-lab/demo2 && cd /tmp/lfs-lab/demo2
git init -b main && git config user.name L && git config user.email l@e.com

# 还不知道有 LFS,直接提交了 3 个版本的 5MB 素材
for i in 1 2 3; do
  dd if=/dev/urandom of=art.psd bs=1m count=5
  git add art.psd && git commit -m "art v$i"
done

git count-objects -vH | grep size:

实测输出:

size: 15.04 MiB          ← 3 个版本 × 5MB,全在仓库里
# 事后才想起来 track
git lfs track "*.psd" && git add .gitattributes && git commit -m "track psd"
dd if=/dev/urandom of=art.psd bs=1m count=5
git add art.psd && git commit -m "art v4 (now via lfs)"

git lfs ls-files          # v4 确实走 LFS 了
git count-objects -vH | grep size:

实测输出:

8c374de024 * art.psd      ← v4 走 LFS 了,看着像成功了

size: 15.06 MiB           ← 但体积一点没降!反而还涨了 0.02 MiB

结论

git lfs track 只影响之后的提交。前 3 个版本仍以真文件形式躺在 git 历史里, 仓库该多大还多大,每个 clone 的人还是要下这 15MB。

一个中间态:--renormalize

如果你只想把当前版本转成 LFS(历史不管),可以:

git add --renormalize .
git commit -m "renormalize"

实测验证(demo3,输出原样粘贴):

1) 提交后才 track:  ls-files -> 0 个            (当前版本仍未走 LFS)
2) renormalize 后:  ls-files -> 3915d5fe7d * a.psd (当前版本转过去了)
3) 但历史里第一版:  /dev/stdin: data            (file 判定为二进制,即真文件)

能解决“以后别再滚雪球”,不能解决“仓库已经很大”。 后者只有第 7 章一条路。


7. 修历史:git lfs migrate 三步法

🚨 这一步会重写 git 历史(所有 commit 的 SHA 全变)。 在共享分支上做 = 所有人必须重新 clone。先在克隆出来的副本上练。

第 1 步:只扫描,不改任何东西

git lfs migrate info --everything

实测输出:

Sorting commits: ..., done.
Examining commits: 100% (5/5), done.
*.psd           16 MB   3/3 files   100%
*.gitattributes 42 B    1/1 file    100%

LFS Objects     5.2 MB  1/1 file    100%

读法:*.psd3 个未 LFS 化的版本、共 16 MB,这就是要迁的东西。

⚠️ --everything 不能省。实测不加它的输出:

*.gitattributes 42 B   1/1 file   100%
LFS Objects     21 MB  4/4 files  100%

*.psd 那一行直接消失了——因为它只看当前分支的当前状态。 不加 --everything 就下结论,会以为「没什么可迁的」。

第 2 步:真正迁移

git lfs migrate import --include="*.psd" --everything

实测输出:

Sorting commits: ..., done.
Rewriting commits: 100% (5/5), done.
Updating refs: ..., done.
Checkout: ..., done.

验证历史里的旧版本也变成指针了:

git cat-file -p HEAD~3:art.psd

实测输出:

version https://git-lfs.github.com/spec/v1
oid sha256:9c821c6bb7d38881bf24c573d81dee84789539375905e2e22dee2376bf4fcd38
size 5242880

第 3 步:清掉不可达的旧对象(这步最容易漏

git count-objects -vH | grep -E "size:|size-pack"     # 迁移后、gc 前

实测输出:

size: 15.10 MiB       ← 迁移完了,体积居然还没降!

旧对象还被 reflog 挂着,得显式清:

git reflog expire --expire-unreachable=now --all
git gc --prune=now
git count-objects -vH | grep -E "size:|size-pack"

实测输出:

size: 0 bytes
size-pack: 2.94 KiB   ← 15.10 MiB → 2.94 KiB

三步法总结

步骤 命令 漏了会怎样
① 扫描 git lfs migrate info --everything 不知道要迁什么,或误判「没东西可迁」
② 迁移 git lfs migrate import --include="..." --everything
③ 清理 git reflog expire ... && git gc --prune=now 体积不降,以为迁移失败了

关于远端:本地瘦了不等于远端瘦了。force push 之后远端旧对象要等它自己 GC, 想立刻回收通常得找管理员。这一条我没有实际验证过(需要真实远端仓库),按官方文档的说法记在这里。

反向操作:把文件从 LFS 拿回普通 git

万一走错了路(比如把不该进 LFS 的小文件放进去了),有 export

git lfs migrate export --include="*.dat" --everything

实测输出:

5475ec8987ad7536a2874da83faa5750c89706e30758c447ff5be9c518a91e93 (2.1 MB), done.
Deleting objects: 100% (1/1), done.

验证:git lfs ls-files 变空,git cat-file 拿到的是真二进制。

⚠️ 一个会让人困惑的细节(实测发现):export 不会删掉原来那行规则, 而是追加一行否定规则

*.dat filter=lfs diff=lfs merge=lfs -text     ← 原来那行还在
*.dat !text !filter !merge !diff              ← 新追加,用 ! 取消上面的属性

看到 filter=lfs 还在别慌,看最后一行——.gitattributes 是后面的规则覆盖前面的。


8. 克隆端发生了什么:smudge / skip / pull

A) 正常 clone —— 真文件自动到位

git clone demo2 clone-normal
ls -lh clone-normal/art.psd

实测:5.0M,是真文件。

B) 跳过 smudge —— 只拿指针

GIT_LFS_SKIP_SMUDGE=1 git clone demo2 clone-skip
cat clone-skip/art.psd

实测输出:

文件大小: 132B
version https://git-lfs.github.com/spec/v1
oid sha256:8c374de02424d504539dab8b9de6a80d38c7100e026f69d860daa735b24bfb79
size 5242880

用途:CI 只需要代码不需要素材时、或者你只想快速看一眼仓库结构——能省掉全部 LFS 流量

C) 事后补齐

git lfs pull

实测:132B → 5.0M

「同事说图片打不开」的两种成因

现象 成因 修复
打开是三行文本 同事没装 git-lfs,smudge 过滤器根本没跑 装 git-lfs → git lfs installgit lfs pull
打开是三行文本 ② 装了,但 clone 时带了 GIT_LFS_SKIP_SMUDGE / 或拉取失败 git lfs pull
图是真文件但不该是 .gitattributes 没提交,同事那边根本没有 LFS 规则,他提交的是真二进制 提交 .gitattributes,然后按第 7 章 migrate

第 ③ 种最阴险:看起来一切正常,仓库在悄悄变胖。


9. 空间与配额治理

本地缓存

LFS 会在 .git/lfs/ 下留一份对象缓存:

du -sh .git/lfs
git lfs prune

实测输出:

缓存: 5.0M	.git/lfs
1 local object, 1 retained, done.

prune 删的是「本地缓存里已经不需要的旧版本对象」,不动远端,可以放心跑。

📌 注意读这次的输出:1 local object, **1 retained** —— 说明这次一个都没删 (那唯一的对象是当前版本,还需要)。真正能看出效果要在一个改过很多版的仓库上跑。 别把「命令跑通了」当成「清出空间了」。

远端配额 ⚠️ 未验证,且对你的仓库不适用

🚨 先说结论:这一节你可以跳过。 实测你的 Unity 仓库 LFS endpoint 是 内网自建服务端.git/config 里的 lfs.http://<内网服务端IP>:10000/<项目名>/...), 不是 GitHub,不受下面这些配额限制。这节留着是为了通用完整性。

以下数字来自我的既有知识,本次没有联网核实,也没有 GitHub LFS 仓库可测。 用到时请以 GitHub 官方计费页面为准。

对 Unity 项目的实际含义:一个正经的 Unity 仓库,美术资源轻松几十 GB, 免费额度基本等于不够用。方案通常是自建服务端(Gitea / GitLab / S3 后端)或购买配额。 选型这块我没有实测数据,只能提示你在开始迁移前先把这件事想清楚。


10. Unity 实战:你的仓库体检

🚨 写这一章前我先去读了你的真实仓库/Users/leo/_Data/_Work/Tap/<项目名>_dev只读,未做任何修改), 结论是:你早就在用 LFS 了,而且配得比通用模板讲究。 所以这章不教你怎么配,只给体检结果具体缺口

⚠️ 检查方法上我踩了两次同样的坑,写在这里以免你复用错方法: 我先用 grep -iE "png|fbx" .gitattributes 查,无输出,差点得出「他没配 png/fbx」的错误结论—— 实际是字符类写法 *.[pP][nN][gG] 字面上没有连续的 “png”,grep 当然搜不到。 后来查 *.bnk 时又栽了一次。查这种 .gitattributes 必须先把字符类解码再比对

grep "filter=lfs" .gitattributes | awk '{print $1}' | sed 's/\[\(.\)[^]]*\]/\1/g'

实测体检结果(2026-08-06)

项目 实测值 判断
是否 git 仓库
.gitattributes 135 行 成熟配置,非默认模板
LFS 跟踪文件数 19,232 个 大规模在用
.git 总占用 34 GB
.git/lfs 缓存 21 GB 占了 .git 的六成
filter=lfs 规则数 48 条
.meta 处理 *.meta text eol=lf 正确,没进 LFS
Unity YAML 合并 .unity/.prefab/.asset 均挂 merge=unityyamlmerge ✅ 已配
.gitignore Temp/ Library/ Logs/ obj/ Build/ 齐全 ✅ 正确
.lfsconfig 不存在(endpoint 记在本机 .git/config ✅ 正常
LFS endpoint 内网自建 <内网服务端IP>:10000 ℹ️ 非 GitHub,第 9 章配额不适用
core.ignorecase true(macOS APFS) ⚠️ 见下文大小写陷阱
大小写覆盖写法 48 条里 30 条用字符类,12 条纯小写 ⚠️ 缺口 1
# 注释的规则 7 条(44–50 行) ⚠️ 缺口 2,需你确认
文件锁 lockable 0 条规则 ⚠️ 缺口 3

你的配置比我的模板好在哪

我原本准备给的模板是 *.psd filter=lfs ...你的仓库(48 条 filter=lfs 规则里的前 30 条)用的是字符类写法

*.7[zZ]              filter=lfs diff=lfs merge=lfs -text
*.[aA][vV][iI]       filter=lfs diff=lfs merge=lfs -text
*.[pP][nN][gG]       filter=lfs diff=lfs merge=lfs -text

这个写法是对的,但我第一版给的理由是错的。 下面是实测。

⚠️ 更正:不是「大小写敏感」,是「行为随平台而变

我第一版写的是「.gitattributes 的模式匹配大小写敏感,所以 Logo.PNG 会漏过 *.png」。 在你的 Mac 上实测,这句话是错的——*.png 照样匹配了 UPPER.PNG

判决性实验(/tmp/lfs-audit,同一份 .gitattributes,只改一个配置):

环境 core.ignorecase *.png 匹配 UPPER.PNG 结果
你的 Mac(APFS 大小写不敏感 true(git 自动设的) ✅ 匹配 正常走 LFS
Linux / 大小写敏感文件系统 false 不匹配 静默变成真二进制进仓库

实测:core.ignorecase=falsegit lfs ls-files 输出为空,git cat-file 拿到的是 data(真二进制)。

所以真实风险比我第一版写的更隐蔽

不是「到处都会漏」,而是「你的 Mac 上不会漏,Linux CI 或 Linux/WSL 开发机上会漏」。 一个在你机器上验证通过的 .gitattributes,换台机器就失效—— 不一致比一致地错更难查,因为你本地永远复现不出来。

结论不变,理由换了:字符类写法 *.[pP][nN][gG] 把大小写全枚举,不依赖平台行为,所以是对的。

怎么自查(以及一个陷阱)

git check-attr filter -- Assets/test.WEM
输出 含义
filter: lfs 走 LFS
filter: unspecified 不走(会以真二进制进仓库)

实测:文件不存在也能查,纯粹按规则推算。你的仓库里 test.WEM 查出来是 filter: lfs

🚨 陷阱:check-attr 反映的是「你这台机器」的 core.ignorecase 实测同一条规则,Mac 上查 UPPER.PNG 得到 filter: lfscore.ignorecase=false 的仓库里查同名文件得到 filter: unspecified所以在你的 Mac 上自查,永远查不出 Linux 那边的问题。 真要验证跨平台,只能在 Linux 机器/CI 上跑一次,或者干脆改成字符类写法一劳永逸。

.meta 的处理也比我说的更精确

我说的是「不进 LFS」,你的配置是:

*.meta               text eol=lf

显式声明为文本 + 强制 LF 换行,比单纯「不写规则」更稳,能防住 Windows 队友的 CRLF 污染。


三个具体缺口(不是「唯一」一个)

📌 第一版这里写的是「唯一的缺口:没有文件锁」——不成立。 那是我只检查了 4 个维度就下的结论。补查之后有 3 项,下面按「是否需要你确认」排序。 而且这仍然不是穷尽检查(13 GB 的非 LFS 对象我没查,见本章末)。

缺口 1:48 条规则里有 12 条没做大小写覆盖(理论隐患,当前未发生

同一份 .gitattributes 里两种写法混用,说明是分两批追加的,后一批没沿用前一批的规范

行号 规则
31–39 *.a *.bundle *.bytes *.keystore *.mdb *.mine *.pdb *.so *.srcaar
52–54 *.unitypackage *.bnk *.wem

实测结论:当前没有实际危害。 我把这 12 个扩展名逐个查了大小写变体 (git ls-files | grep -iE "\.wem$" | grep -vE "\.wem$" 之类),一个都没有

🚨 更正我第一版的判断:那一版我写「52–54 行风险最高,.WEM/.Bytes 最容易出现」—— 这是没查就下的判断,实测不成立。 真实情况是这 12 类文件目前全是小写扩展名, 所以哪怕在 Linux 上也不会漏。它是隐患(哪天有人交个 .WEM 就中招),不是现存问题。

对照组:.FBX 这类大写文件真实存在 771 个,但它们被字符类规则 *.[fF][bB][xX] 覆盖着, 所以安全——这恰好证明字符类写法当初是被真实需求逼出来的,不是过度设计。

另外 *.bnk 定义了两次*.[bB][nN][kK] 和第 53 行的 *.bnk)。无害,是分批追加的又一证据。

缺口 2:44–50 行有 7 条规则# 注释掉了 ⚠️ 需要你确认

44:#client/Assets/SDK/SDKFacebook/.../Bolts	filter=lfs diff=lfs merge=lfs -text
45:#client/Assets/SDK/SDKFacebook/.../FBSDKCoreKit	filter=lfs ...
46:#client/Assets/SDK/SDKFacebook/.../FBSDKLoginKit	filter=lfs ...
47:#client/Assets/SDK/SDKFacebook/.../FBSDKShareKit	filter=lfs ...
48:#client/Map/7200/mapData.config filter=lfs ...
49:#client/Map/7200_empty/mapData.config filter=lfs ...
50:#client/Assets/TFWCore/**	-filter diff merge      ← 这条是「排除 LFS」,也被注掉了

#.gitattributes 里是注释符,这 7 条一条都不生效。

已查清(2026-08-06 实读):仓库根目录没有 client/,现在是 Assets/ Map/ Packages/ DLC/ … 的结构。 所以这 7 条是仓库改结构后留下的历史遗留,路径前缀 client/ 早已对不上—— 即使取消注释也不会生效。逐条追查结果:

规则 目标现状 结论
44–47 Facebook SDK 的 4 个 .framework 已从仓库删除find 无结果) ✅ 无害,可清理
50 TFWCore/** -filter(排除 LFS) 现在在 Assets/P2/Editor/TFWCore,478 个文件全是 .cs/.meta 文本 ✅ 无害——本来就没有规则会匹配它们,排除与否结果相同
48–49 Map/*/mapData.config(本意:应走 LFS) 文件仍在,且 filter: unspecified ⚠️ 本意未达成,见下

唯一有实质影响的是 48–49 行

Map/7200/mapData.config          148 KB   filter: unspecified
Map/7200_empty/mapData.config    3.9 MB   filter: unspecified   ← 以真二进制存在 git 里

当年有人写规则想让它走 LFS,后来规则连同路径一起失效了,这两个文件至今是普通 git 对象。 不过危害有限:实测 git log 显示相关 .config 文件只有 1 个版本(没被反复修改), 所以它占的是「一次性 4 MB」而不是「4 MB × N 版」。

缺口 3:没有文件锁 lockable(0 条规则)

你已经配了 merge=unityyamlmerge,而且 .gitattributes 里那句注释写得很清楚:

# 场景/Prefab/Asset 额外挂 UnityYAMLMerge(团队需在 ~/.gitconfig 配 driver;未配会 fallback 普通 merge,不阻塞)

但 YAML 合并只是「尽力而为」,lockable 才是「从源头避免」。 两者解决的是同一个问题的两端:

手段 作用时机 局限
merge=unityyamlmerge 冲突发生后尝试自动合并 复杂场景/prefab 仍会失败;队友没配 driver 就直接退化
lockable + git lfs lock 冲突发生前独占 需要服务端支持锁 API

加锁的写法是在现有规则后追加 lockable

*.unity              text eol=lf merge=unityyamlmerge lockable

对应的日常命令:

git lfs lock   Assets/Scenes/Main.unity   # 锁定,别人推不上去
git lfs locks                             # 看谁锁了什么
git lfs unlock Assets/Scenes/Main.unity   # 解锁

⚠️ 这套流程我完全没验证过。实测到你的 LFS endpoint 是内网自建服务端 (http://<内网服务端IP>:10000/<项目名>/<项目名>.git,端口 10000,看着像 Gitea 但我没确认), 它支不支持锁 API,只有问管服务器的人或者试一次才知道。 别直接改生产的 .gitattributes,先在测试仓库走通。

另外 lockable 会让未锁定的文件在本地变成只读,这会改变全组的日常手感—— 属于团队约定,不是你一个人能定的事。

立刻能做、零风险的一件事

你的 .git/lfs 缓存有 21 GB。第 9 章那个 git lfs prune 在这里才真正有意义 (我在 demo 仓库跑时是 1 retained,一个都没删——那种输出说明不了任何问题):

cd /Users/leo/_Data/_Work/Tap/<项目>_dev
git lfs prune --dry-run     # ← 先看它打算删什么,不动手

实测输出(2026-08-06):

35125 local objects, 16429 retained, done.
18698 files would be pruned (18 GB), done.

21 GB 缓存里 18 GB 可清.git 会从 34 GB 降到约 16 GB。 内网 LFS 服务端 <内网服务端IP>:10000 实测可达(prune 需要它验证对象已上传)。

📌 但你并不缺空间:实测磁盘剩余 860 GB(该仓库工作区+仓库共占 193 GB)。 所以这件事是「顺手可做」,不是「该做」。 别因为数字大就觉得非清不可—— 清掉的是本地缓存,下次 checkout 到老版本时会重新下载。

确认无误后再去掉 --dry-run这个操作只动本地缓存,不动远端,也不改历史, 是全文所有「会改变东西」的操作里风险最低的一条(纯只读的 migrate infocheck-attr 当然更安全)。

全仓库扫描:谁漏了 LFS(2026-08-06 实测,10 万文件全查)

用的方法(纯只读,不改仓库):

git ls-files -z > /tmp/tracked.z
git check-attr --stdin -z filter < /tmp/tracked.z | tr '\0' '\n' | paste -d'\t' - - -

归属分布(100,137 个跟踪文件)

filter 值 数量 含义
lfs 19,242 走 LFS ✅
unspecified 80,404 不走(绝大多数是 .cs/.meta 等文本,正常)
unset 491 Packages/com.tfw.avprovideo/** -filter 明确排除(第 57 行,规则有效)

>1 MB 且不走 LFS 的文件,按扩展名汇总(工作区 2915 个大文件里筛出):

体积 个数 扩展名 性质判断
825.1 MB 340 .prefab 🟡 Unity YAML 文本,不该进 LFS(进了就没法 merge),但单个 29 MB 是设计问题
278.3 MB 16 (无扩展名) 🔴 真该进 LFS 却漏了——Wwise 的 dSYM/DWARF、.bundle 目录里的 Mach-O
193.6 MB 85 .tsv 🟡 文本配置,git 压缩尚可,不建议进 LFS
189.3 MB 18 .asset 🟡 Terrain 数据(17.6 MB × 8),文本 YAML
142.0 MB 23 .json 🟡 同 .tsv
119.5 MB 4 .tgz 🔴 该进 LFS 或干脆不该进仓库(包管理器产物)
87.9 MB 7 .config 🔴 含上文那两个 mapData.config + 68 MB 的 polygonRiverLayer.config
63.3 MB 6 .unity 🟡 场景文本
18.7 MB 2 .dylib 🔴 规则有 *.so 却漏了 *.dylib

🔴 最值得注意的一个坑:*.bundle 规则形同虚设

.gitattributes 里有 *.bundle filter=lfs,但实测 AkUnitySoundEngine.bundle/Contents/MacOS/AkUnitySoundEngine (23.7 MB)没走 LFS。原因:

macOS 上 .bundle 是目录不是文件。 git 只跟踪文件, 而目录里的实际文件叫 AkUnitySoundEngine——文件名不带 .bundle 后缀,规则匹配不上。

同理 .framework.dSYM 也都是目录。凡是「后缀其实是目录名」的格式,*.xxx 规则一律无效, 要改成路径模式,例如 **/*.bundle/**。这个坑对 Unity + Wwise/iOS 项目非常典型。

但危害没有想象中大:实测这些文件都只有 1 个版本

文件 体积 历史版本数
com.google.firebase.app-12.2.0.tgz 90 MB 1
Map/7200_level/polygonRiverLayer.config 68 MB 1
librealm-wrappers.dylib 15 MB 1
21201116.prefab(最大的 prefab) 29 MB 2

按第 1 章的判据——危害取决于「改过多少版」,不是「多大」——这些是「一次性占用」, 不是「滚雪球」。所以这不是紧急问题,值得修但不必今天修。

⚠️ git log -- <file> 的版本数在文件被 rename/move 时会断,这里只作量级参考,未加 --follow


11. 反模式清单

# 反模式 后果 正确做法
1 先提交大文件,事后 track 仓库体积一点不降(实测 15.04→15.06 MiB) git lfs migrate import --everything
2 .gitattributes 忘了提交 队友提交的是真二进制,仓库悄悄变胖 git add .gitattributes 和首次 track 一起提交
3 git lfs track *.psd(无引号) shell 展开成死文件名,新文件不生效 永远加引号
4 migrate info 不加 --everything 漏看历史,误判「没东西可迁」(实测整行消失) 永远加 --everything
5 migrate 后不 gc 体积不降,误以为迁移失败 reflog expire + gc --prune=now
6 在共享分支上直接 migrate + force push 全组本地仓库作废 先在副本上练,约好时间再推
7 .meta 塞进 LFS Unity 资产系统受累,无收益 只有二进制大文件进 LFS (你已做对)
8 Library/ 放进 LFS 该 ignore 的东西进了仓库 Library/.gitignore (你已做对)
9 不管配额就往里推 超额被限制,团队推不上去 动手前算清存储 + 带宽 (你是内网自建,不适用)
10 以为 LFS 会让 clone 变快 期待落空 它减的是历史包袱,不是当前文件体积
11 规则写 *.png 而不是 *.[pP][nN][gG] Mac 上没事、Linux/CI 上静默漏过,最难查 字符类枚举大小写,不依赖平台
12 小文件 / 纯文本塞进 LFS 多一次网络往返且丢掉 diff 能力,收益为负 判据是「会不会被反复改」,不是「大不大」
13 grep png 检查字符类写法的 .gitattributes 静默漏报,会误判「没配」 sed 解码字符类再比对(第 10 章有命令)

12. 命令速查表

# ---- 环境 ----
git lfs version                        # 装没装
git lfs install                        # 每台机器一次
git lfs env                            # 看当前仓库的完整 LFS 配置

# ---- 日常 ----
git lfs track "*.psd"                  # 声明(记得加引号)
git lfs track                          # 列出当前所有规则
git lfs untrack "*.psd"                # 取消声明
git lfs ls-files                       # 当前版本哪些走了 LFS  ← 第一诊断命令
git lfs ls-files --all                 # 所有历史版本
git lfs status                         # 待提交的 LFS 变更

# ---- 验证 ----
git cat-file -p HEAD:文件名             # 是指针还是二进制?  ← 最硬的判据
git count-objects -vH                  # 仓库体积

# ---- 克隆端 ----
GIT_LFS_SKIP_SMUDGE=1 git clone URL    # 只拉指针,省流量
git lfs pull                           # 补齐真文件
git lfs fetch --recent                 # 只拉最近的历史版本

# ---- 治理 ----
git lfs migrate info --everything                        # ① 扫描
git lfs migrate import --include="*.psd" --everything    # ② 迁移
git reflog expire --expire-unreachable=now --all         # ③ 清理
git gc --prune=now
git lfs prune --dry-run                # 先看要删什么(推荐先跑这个)
git lfs prune                          # 清本地缓存(不动远端,安全)
git lfs migrate export --include="*.dat" --everything    # 反向:退出 LFS

# ---- 排查 .gitattributes(字符类写法用 grep 会漏报)----
grep "filter=lfs" .gitattributes | awk '{print $1}' | sed 's/\[\(.\)[^]]*\]/\1/g'
git check-attr filter -- 文件名        # 直接问 git:这个文件走不走 LFS(文件不存在也能查)
#   输出 "filter: lfs"        = 走
#   输出 "filter: unspecified" = 不走
#   ⚠️ 它反映【当前机器】的 core.ignorecase,Mac 上查不出 Linux 的大小写问题(见第 10 章)

# ---- Unity 协作(需服务端支持,未验证)----
git lfs lock 文件 / git lfs locks / git lfs unlock 文件

13. 自测题

按文档里方法③的规矩,一次只答一道,答完再看下一道。答不上来的回对应章节。

  1. 为什么已经提交过的大文件,事后 git lfs track 救不回仓库体积?(→ 第 1、6 章)
  2. 怎么用一条命令判断某个文件到底有没有走 LFS?(→ 第 3 章)
  3. 同事说「图片打不开,内容是三行文本」,列出两种不同成因和各自的修复。(→ 第 8 章)
  4. git lfs migrate info 不加 --everything 会漏掉什么?(→ 第 7 章)
  5. migrate 跑完了,git count-objects 显示体积没变,出了什么问题?(→ 第 7 章第 3 步)
  6. 什么情况下你会主动用 GIT_LFS_SKIP_SMUDGE=1?(→ 第 8 章)
  7. *.png 这种写法在什么情况下会漏掉 Logo.PNG、什么情况下不会?为什么这种「有时漏有时不漏」比「总是漏」更难查?(→ 第 10 章)
  8. 你在 Mac 上跑 git check-attr filter -- Logo.PNG 得到 filter: lfs,能否据此断定这条规则在全组都没问题?(→ 第 10 章)
  9. (针对你的仓库) 已经配了 merge=unityyamlmerge,为什么还需要 lockable?两者分别在什么时机起作用?(→ 第 10 章)
  10. 一个 500 MB 但只提交过一次的安装包,和一个 5 MB 但改了 200 版的贴图,哪个更该进 LFS?(→ 第 1 章)

14. 学习路径

30 分钟版(够用了)

时间 做什么
5 min 第 1 章心智模型 + 第 2 章确认环境
10 min 照第 3 章敲一遍,敲到 git cat-file 看见三行指针为止
10 min 照第 6 章复现一次事故,亲眼看见 15.04 → 15.06
5 min 扫一眼第 11 章反模式清单

2 小时版(要动真仓库就走这个)

时长 做什么
30 min 30 分钟版全部
30 min 第 7 章 migrate 三步法,在 /tmp 副本上完整跑通,包括 gc 后的体积对比
20 min 第 8 章:自己 clone 两遍(正常 / skip smudge),把三种「打不开」的成因验一遍
40 min 读第 10 章体检报告 → 在主仓库跑 git lfs prune --dry-run(零风险)→ 想清楚 lockable 要不要上(这是团队决策,不是技术决策)

清理实验目录

rm -rf /tmp/lfs-lab

附:本文档的验证状态

章节 验证状态
1–8 章、11–12 章 本机实测(2026-08-06, git-lfs 3.7.1,/tmp/lfs-lab/tmp/lfs-exp
7 章 migrate export ✅ 实测,含「.gitattributes 追加否定行而非删除原行」这个细节
10 章 大小写跨平台差异 判决性实测/tmp/lfs-auditlfs-audit2,只改 core.ignorecase 一个变量)
10 章 check-attr 及其局限 ✅ 三个环境各测一次
10 章「体检结果」全表 读了你的真实仓库(只读,未修改):135 行 / 48 条规则 / 19232 文件 / 34G / 21G
7 章「远端 GC」 ⚠️ 未验证,需真实远端仓库
9 章 GitHub 配额数字 ⚠️ 未联网核实,且对你不适用(你是内网自建 endpoint)
10 章 文件锁 ⚠️ 未验证;你的服务端是否支持锁 API 我不知道<内网服务端IP>:10000,看着像 Gitea 但没确认)
10 章 缺口 2(7 条被注释的规则) 已查清client/ 不存在→历史遗留;逐条追踪了 3 类目标的现状
10 章 全仓库扫描 10 万文件全查check-attr --stdin,只读):19242 lfs / 80404 unspecified / 491 unset
10 章 漏网文件版本数 ⚠️ 用 git log -- 统计,未加 --follow,rename 会断;只作量级参考
10 章「.git 里 13 GB 的精确构成」 ⚠️ 仍未精确拆解。从扫描结果推断主要是文本资产(prefab/asset/json/tsv)的历史累积,
但没跑 migrate info --everything(24242 次提交,耗时长且属于要动主仓库的操作)。这是推断不是实测。

修订记录


← 返回 AI 编程