你 clone 了仓库,在 Photoshop 里打开 assets/hero-image.psd,结果它瞬间崩溃。运行 file assets/hero-image.psd 真相就出来了:这根本不是 PSD,而是一个 ASCII 文本文件,第一行写着 version https://git-lfs.github.com/spec/v1。这三行文本就是 Git LFS pointer——真实的二进制内容存在 LFS 服务器上,本该在 checkout 时把 pointer 换成真实文件的 smudge filter 没有运行。
最快的修复(覆盖最常见的原因):
git lfs install # 注册 smudge/clean filter
git lfs pull # 下载当前 commit 引用的所有 LFS 对象
如果文件恢复正常,到这就结束了。如果 git lfs pull 报错、或者文件还是 pointer 文本,说明 smudge filter 被跳过,或者 LFS 服务器拒绝了你的请求——按下面的排查表定位你属于哪一类。本文命令与参数对应 Git LFS 3.7.x(2026 年 6 月),均为当前可用版本。
你属于哪一类
先跑这几个检查,结果会告诉你该看哪一节原因。
| 检查 | 指向问题的输出 | 对应原因 |
|---|---|---|
git lfs version | command not found 或无输出 | LFS 未安装(原因 1) |
git config filter.lfs.smudge | 为空,或含有 --skip | smudge filter 未注册 / 被跳过(原因 1、2) |
git lfs pull 2>&1 | tail -20 | batch response: This repository is over its data quota. 或 401/403 | 服务器配额或认证(原因 3、4) |
git lfs env 的 Endpoint= 不对 | 迁移后 URL 还指向旧主机 | endpoint 错误(原因 5) |
git lfs ls-files 里没有你预期的文件 | 该文件是普通 blob,不是 LFS | .gitattributes 规则加晚了(原因 6) |
常见原因
按命中率从高到低排列。
1. Git LFS 未安装,或从没执行过 git lfs install
clone 时本机没有 git-lfs 程序,或者装了但从没运行 git lfs install(这条命令会把全局的 filter.lfs.* 配置写进去)。Git 看到 .gitattributes 里的 filter=lfs 属性,却找不到对应的 filter,于是直接 checkout 出原始的 pointer 文本。
怎么判断:git lfs version 报 “command not found” 或版本低于仓库要求。git config filter.lfs.smudge 输出为空。
2. smudge filter 被设成跳过(环境变量或持久化配置)
同一个问题的两种形态。临时的那种是 clone 时所在的 shell 或 CI step 里设了 GIT_LFS_SKIP_SMUDGE=1。持久的那种更隐蔽:有人跑过 git lfs install --skip-smudge,它会把 filter.lfs.smudge = git-lfs smudge --skip -- %f 和 filter.lfs.process = git-lfs filter-process --skip 写进全局配置,之后你每次 clone 都会停在 pointer 文本,直到手动改回来。
怎么判断:echo $GIT_LFS_SKIP_SMUDGE 输出 1,或者 git config filter.lfs.smudge 里含有 --skip。CI 日志里 clone 飞快、却没有任何 LFS 下载,也是一个信号。
3. LFS 服务器配额用尽、被限流、或返回 403
在 GitHub 上,LFS bandwidth 配额一旦用尽,当月剩余时间 LFS 都会被禁用,batch API 随即返回错误,smudge filter 退回去写 pointer 文本。截至 2026 年 6 月,GitHub Free 和 Pro 账户每月各有 10 GiB 的 LFS 存储和 10 GiB 的 bandwidth,Team 和 Enterprise Cloud 各 250 GiB。GitHub 已经取消了旧的预付费「data pack」,改成按用量计费(超出额度后按 GiB 收费),所以「买个 data pack」的老办法不再适用。
怎么判断:git lfs pull 打印 batch response: This repository is over its data quota.,或者运行 GIT_CURL_VERBOSE=1 git lfs pull 2>&1 | grep -E "403|429|quota|rate limit",看 LFS endpoint 是否返回了任何非 200 的响应。
4. LFS 认证缺失或权限范围不对
clone 用的凭证没有 LFS 读取权限、保存的 token 过期,或者私有模式下的自建/Enterprise 服务器拒绝了 batch POST。此时 git lfs pull 会在 /info/lfs/objects/batch 上报 401 Unauthorized 或 403 Forbidden。
怎么判断:git lfs pull 2>&1 | head -20 出现 401/403。GitHub 上 fine-grained 或 classic token 需要 contents: read(或 repo scope)才能访问 LFS。对于 Enterprise private mode,把 remote 换成 SSH 通常能绕过 batch 403。
5. 仓库迁移后 LFS endpoint 指向了错误的服务器
仓库从 GitHub 迁到 GitLab(或迁到自建 LFS 服务器),但 .lfsconfig 或 remote.origin.lfsurl 还是旧地址,下载就悄悄打到了错误的服务器。
怎么判断:git lfs env 看 Endpoint= 那一行,确认它指向当前主机。git lfs push --dry-run 也会暴露连到旧主机的连接错误。
6. .gitattributes 的 LFS 追踪规则缺失或加得太晚
文件在 filter=lfs 规则加进 .gitattributes 之前,已经作为普通 blob 提交过了。现在的 .gitattributes 有规则,但历史里那份 blob 从没被迁移,于是旧版本 checkout 出来是普通内容(常常看着像被截断),新版本却是 pointer——混在一起让人摸不着头脑。
怎么判断:git lfs ls-files 里没有你预期的文件。git log --oneline -- .gitattributes | tail -5 显示追踪规则是在该文件首次提交之后才加的。git cat-file -p HEAD:path/to/file | head -1 若第一行不是 version https://git-lfs...,说明它是普通 blob。
最短修复路径
Step 1:安装并初始化 Git LFS
# macOS
brew install git-lfs
# Debian/Ubuntu
sudo apt-get install git-lfs
# 或从 https://git-lfs.com 下载
git lfs install # 把 smudge/clean filter 注册到全局配置
Step 2:拉取当前 checkout 的所有 LFS 对象
git lfs pull
它会下载当前 commit 引用的每一个 LFS 对象,并运行 smudge filter 把 pointer 文件替换成真实内容。想省带宽就限定范围:
git lfs pull --include="assets/hero-image.psd"
git lfs pull --include="assets/**"
Step 3:撤销被跳过的 smudge filter
如果 git config filter.lfs.smudge 里含有 --skip,重新启用并重新 checkout:
git lfs install --force # 重写 filter.lfs.*,去掉 --skip
unset GIT_LFS_SKIP_SMUDGE # 清掉当前 shell 的环境变量
git lfs pull # 下载对象
git checkout -- . # 对工作区重新跑一遍 smudge
Step 4:修复认证
# 清掉缓存的凭证再重试
git credential reject <<EOF
protocol=https
host=github.com
EOF
git lfs pull
如果自建/Enterprise 服务器在 HTTPS 上对 batch endpoint 返回 403,把 remote 换成 SSH:
git remote set-url origin git@github.example.com:org/repo.git
git lfs pull
Step 5:迁移后修复 LFS endpoint URL
git lfs env # 确认当前的 Endpoint=
git config lfs.url https://gitlab.example.com/org/repo.git/info/lfs
git lfs pull
或者在仓库根目录的 .lfsconfig 里固定下来,免得以后迁移又出问题:
[lfs]
url = https://gitlab.example.com/org/repo.git/info/lfs
Step 6:把历史里的普通 blob 迁移成 LFS
针对原因 6 那种旧版本是普通 blob 的情况:
git lfs migrate import --include="*.psd" --everything
git push --force-with-lease --all
git push --force-with-lease --tags
这会重写历史,记得通知所有协作者迁移后重新 clone(或 hard reset)。
Step 7:验证已修复
file assets/hero-image.psd # 应输出 "Adobe Photoshop Image",而不是 "ASCII text"
git lfs status # 被追踪的文件不应再被列为 pointer
git lfs ls-files # 每个文件应显示 "*"(对象已在本地)
如果某个文件还是不对,把 smudge 过程的调试日志抓下来:
GIT_TRACE=1 GIT_LFS_LOG_FILE=lfs-debug.log git checkout -- assets/hero-image.psd
# 然后查看 lfs-debug.log 里服务器返回的确切内容
预防建议
- 把
git lfs install写成仓库安装说明和 onboarding 脚本的第一行。缺这一步,该贡献者所有 LFS 文件都会变成 pointer 文本。 - CI 里
actions/checkout的默认值截至 2026 年 6 月仍是lfs: false。要么设lfs: true,要么在 checkout 之后显式加一个git lfs pullstep(并在自定义镜像里预装git-lfs——ubuntu-latest自带,但如果依赖某个新 flag 就锁定一个已知版本)。 - LFS 仓库一律用普通
git clone。并行 LFS 下载现在已内置进核心 clone;git lfs clone已废弃并会打印警告。超大仓库可以先用GIT_LFS_SKIP_SMUDGE=1clone,再用git lfs pull --include=...定向拉取来控制带宽。 - 提交一个带显式
lfs.url的.lfsconfig,让 endpoint 在 remote 迁移后依然有效。 - 在 LFS bandwidth 触及月度上限(截至 2026 年 6 月 GitHub Free/Pro 为 10 GiB)之前就盯紧用量——一旦被禁用,要等到下个计费周期才会恢复。
- 在提交任何大二进制文件之前先用
git lfs track把规则加进.gitattributes,绝不要提交完再补规则。
常见问答 (FAQ)
Q: 不打开文件,怎么判断它是 LFS pointer 还是真实文件?
A: git cat-file -p HEAD:assets/hero-image.psd | head -3。如果第一行是 version https://git-lfs.github.com/spec/v1,就是 pointer。也可以 file <path>:明明该是二进制却报 ASCII text,那就是 pointer。
Q: git lfs pull 报 “Object does not exist on the server”,可文件明明被追踪了,怎么回事?
A: 二进制从没被推上去——有人提交了 pointer 却漏了在自己机器上跑 git lfs push origin --all。让他补推,或者你从别处恢复这个二进制。注意这跟配额错误不同,配额错误会说 This repository is over its data quota。
Q: GitHub 的 LFS bandwidth 配额爆了,有哪些选择?
A: 截至 2026 年 6 月 GitHub 已改用按量计费,最简单的办法是绑定支付方式、按 GiB 付超额费用(旧的预付费 data pack 已经没有了)。要彻底避开 GitHub 带宽,可以把 .lfsconfig 里的 lfs.url 指向自建 LFS 存储(比如基于 S3/R2 的 lfs-s3 服务器,或 Gitea 的 LFS endpoint),再把对象推到那里。
Q: git lfs fetch 和 git lfs pull 有什么区别?
A: git lfs fetch 只把对象下载到本地缓存(.git/lfs/objects/),不动工作区;git lfs pull = git lfs fetch + git lfs checkout,会把真实内容写进文件。日常用 pull。
Q: Git LFS 支持 shallow / partial clone 吗?
A: 支持,但有注意点。git clone --depth 1 之后再 git lfs pull 能解析浅历史里的 commit,更深历史里的对象在 unshallow 之前仍取不到。partial clone(--filter=blob:none)从 Git LFS 3.6+ 起配合得更好,但如果 pointer 仍然存在,checkout 后显式跑一次 git lfs pull。
相关阅读
- 二进制文件合并冲突 — 手动无法 resolve
- 历史里的大文件让 push 失败
- Monorepo partial clone 数据过期
- Submodule 怎么都拉不到最新 commit
- Git commit 改坏了,怎么找回旧版本
标签: #git #version-control #排查