Git LFS pointer 文件没被真实文件替换

clone 后图片、模型等大文件变成几行 LFS pointer 文本,程序报「无效文件格式」。先 git lfs install 再 git lfs pull 两条命令解决,并附上 smudge filter 为什么没跑的排查表。

你 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 versioncommand not found 或无输出LFS 未安装(原因 1)
git config filter.lfs.smudge为空,或含有 --skipsmudge filter 未注册 / 被跳过(原因 1、2)
git lfs pull 2>&1 | tail -20batch response: This repository is over its data quota.401/403服务器配额或认证(原因 3、4)
git lfs envEndpoint= 不对迁移后 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 -- %ffilter.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 Unauthorized403 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 服务器),但 .lfsconfigremote.origin.lfsurl 还是旧地址,下载就悄悄打到了错误的服务器。

怎么判断:git lfs envEndpoint= 那一行,确认它指向当前主机。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 pull step(并在自定义镜像里预装 git-lfs——ubuntu-latest 自带,但如果依赖某个新 flag 就锁定一个已知版本)。
  • LFS 仓库一律用普通 git clone。并行 LFS 下载现在已内置进核心 clone;git lfs clone 已废弃并会打印警告。超大仓库可以先用 GIT_LFS_SKIP_SMUDGE=1 clone,再用 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 fetchgit 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

相关阅读

标签: #git #version-control #排查