给本地裸仓库启用 Git LFS:1.6GB 媒体历史迁移实录

十月 01, 2026 [杂项] #Git #迁移

背景

一个生日视频剪辑项目,仓库里堆了近 200 个手机拍摄的照片/视频,仓库结构是"本地裸仓库做远端":

问题:历史里的 mp4/jpg 全是普通 git blob,裸仓库已经膨胀到 1.60 GiB,每次全量克隆都要拖走全部历史。目标是让这个仓库支持 Git LFS,并且把已有历史里的媒体也迁进 LFS。

关键认知:LFS 对象传输不走 git 智能协议

LFS 最容易被误解的一点:大文件内容不在 git 对象库里(库里只存几十字节的指针文件),传输走的是独立通道。服务端要求取决于远端形式,分三档:

远端形式服务端要求传输机制
本地路径 / file://零配置lfs-standalone-file 传输代理,直接写 <仓库>/lfs/objects/
ssh://git@host/repo(裸仓库)服务器装 git-lfs ≥ 3.0 即可客户端经 SSH 执行 git-lfs-transfer,对象走 SSH 通道落盘服务器仓库的 lfs/objects/,认证就是 SSH 公钥,无需 HTTP 服务
GitHub / GitLab / Gitea 等平台平台内置HTTPS LFS API(<repo>/info/lfs)

也就是说 ssh://git@host/repo 是原生支持的,条件只是服务器上那个 git 用户能执行 git-lfs(3.0+,apt 装的版本一般满足)。若服务器没装,push 会报 git-lfs-authenticate: command not found。另外 standalone 模式(本地/SSH)不支持 git lfs lock 文件锁,要锁就得用托管平台或自建 LFS API。

本地路径场景可以用 git lfs env 验证,能看到传输代理列表里有 lfs-standalone-file、endpoint 直接指向裸仓库且无需认证:

$ git lfs env | grep -iE 'endpoint|UploadTransfers'
Endpoint=file:///home/peter/project/repos/birthday (auth=none)
UploadTransfers=basic,lfs-standalone-file,ssh

迁移全流程

1. 初始化 + 设置跟踪规则

cd ~/project/birthday
git lfs install                       # 全局装 smudge/clean 过滤器(每台机器一次)
git lfs track "*.mp4" "*.jpg" "*.jpeg" "*.png" "*.mov" "*.gif"
git add .gitattributes && git commit -m "chore: 启用 git lfs"

git lfs track 本质是往 .gitattributes 写过滤器规则:

*.mp4 filter=lfs diff=lfs merge=lfs -text
*.jpg filter=lfs diff=lfs merge=lfs -text
...

时机坑:如果一个大文件在 track 之前就已经 git add 进了 index,index 里存的还是全量 blob(add 时刻就完成了 clean 过滤)。必须重新 git add 一次让 LFS 过滤器生效,否则提交进去仍是完整大文件。

2. 重归一化,让工作树变干净

track 之后 git status 会冒出一大堆 M 的媒体文件——不是真的被改了,而是 HEAD 里还是全量 blob、工作区文件经过滤器比对出了差异。用 renormalize 提交消除:

git add --renormalize .
git commit -m "chore: 已跟踪媒体文件重归一化为 LFS 指针"

3. 重写全部历史

git lfs migrate import --everything --include="*.mp4,*.jpg,*.jpeg,*.png,*.mov,*.gif"
migrate: Rewriting commits: 100% (16/16), done.
migrate: Updating refs: ..., done.
migrate: checkout: ..., done.

16 个提交全部重写、哈希全变,历史中的媒体 blob 替换为 LFS 指针。抽查重写后最早那条素材提交,照片内容已经是指针:

$ git cat-file -p b80dd68:pictures/IMG_20260923_113450.jpg
version https://git-lfs.github.com/spec/v1
oid sha256:a69f81f1a10188c9381d1e29d9b922ea3bc0a19befe105f9b5e1439869840392
size 5849437

4. force push + 两边 gc

$ git push --force origin master
Uploading LFS objects: 100% (187/187), 0 B | 0 B/s, done.
To /home/peter/project/repos/birthday
 + 64e2e62...1449fb0 master -> master (forced update)

187 个 LFS 对象(1.9G)经 standalone 传输直接落盘裸仓库 lfs/objects/。随后两边回收旧 blob:

git -C ~/project/repos/birthday gc --prune=now           # 裸仓库
git reflog expire --expire-unreachable=now --all && git gc --prune=now   # 工作副本

5. 验收:全新克隆 + sha256 对比

GIT_LFS_SKIP_SMUDGE=1 git clone ~/project/repos/birthday /tmp/verify   # 秒级,只拿指针
cd /tmp/verify
git lfs pull --include=pictures/IMG_20260923_113450.jpg                # 按需拉单个对象
sha256sum pictures/IMG_20260923_113450.jpg
# a69f81f1...0392 —— 与源文件、与指针 oid 三方一致

效果

项目迁移前迁移后
裸仓库 git 对象(size-pack)1.60 GiB123.03 KiB
媒体存储git blob(进每次克隆)lfs/objects/ 187 个对象,1.9G
全新克隆必拉 1.6G 历史秒级,媒体按需 git lfs pull

基础配置与常用命令速查

配置类

命令作用
git lfs install全局装 smudge/clean 过滤器 + 本仓库 hooks(pre-push 等),每台机器一次
git lfs track "*.mp4"添加跟踪规则(写 .gitattributes),之后 add 的文件才走 LFS
git lfs track不带参数:列出当前所有跟踪规则
git lfs untrack "*.mp4"移除规则
git add --renormalize .规则变更后,把已跟踪文件按新过滤器重新入库(老文件补转 LFS 指针)
git lfs env查看 endpoint、可用传输代理等运行时配置

查看状态类

命令作用
git lfs ls-files列出当前受 LFS 管理的文件(oid 前缀 + 路径)
git lfs ls-files -n只列路径(配合 grep/wc 好用)
git lfs ls-files -s附带对象大小
git lfs track列出跟踪规则本身(区别于上面列文件)
git check-attr filter -- <path>验证单个路径是否命中规则:filter: lfs 命中,unspecified 未命中

传输类

命令作用
git push / git pull正常用即可,hooks 自动传输 LFS 对象,日常无感
git lfs pull把当前 commit 需要的对象拉全并填充工作区
git lfs fetch --all拉全量对象(不含填充工作区)
git lfs push origin master --all把本地全部 LFS 对象推到远端(备份场景)
GIT_LFS_SKIP_SMUDGE=1 git clone <url>克隆时不拉媒体,先拿代码;之后 git lfs pull / git lfs pull --include="<路径>" 按需拉
git lfs logs last传输失败时看最近错误日志

历史迁移与空间类

命令作用
git lfs migrate info --everything迁移前分析:按大小列出值得转 LFS 的文件类型
git lfs migrate import --everything --include="*.mp4,*.jpg"重写全部历史(哈希会变,需 force push)
git lfs prune清理本地 .git/lfs/objects 中远端已持有的对象缓存(裸仓库已是全量备份时安全)
git count-objects -vH看 git 对象库体积(迁移前后对比用)

踩坑记录

坑 1:migrate 把未跟踪文件也算 dirty

git lfs migrate import 内部用 git status --porcelain 判断工作区是否干净——未跟踪文件(??)也算。几个 __pycache__/*.pyc 就会触发确认:

migrate: override changes in your working copy?  All uncommitted changes will be lost! [y/N]
migrate: working copy must not be dirty

非交互环境下默认选 N 直接中止。解法:先把该忽略的写进 .gitignore 并提交,git status --porcelain 输出为空再跑 migrate。顺带一提 git status --short 里的未跟踪文件和 git lfs migrate 的判定是一致的。

坑 2:验证历史时误用重写前的旧哈希

gc 之前旧对象还在对象库里,git cat-file -p <旧哈希>:<路径> 依然能查到——查出来是原始二进制。验证迁移效果必须先通过 git log --diff-filter=A --format=%H -- <路径> 拿到重写后的新哈希再查。

这个坑还连带了终端事故:二进制直接喷进终端,乱码 + 终端状态损坏(错位、字符集错乱)。恢复手段的差别:

命令作用
clear只清屏,不修复终端状态
tput reset / reset重新初始化终端:回显、字符集、键位、制表位全恢复
stty sane只修复行纪律层(回显、控制键)

先试 printf '\e[0m\e[?25h'(重置样式 + 恢复光标),不行再 reset。预防:看可疑对象先 file 判断类型,或过 xxd/strings/head -c 管道。

坑 3:track 之前已暂存的文件不会自动转

前文"时机坑"的展开:git add 在暂存时刻就完成 clean 过滤并落了 blob,后配置的 .gitattributes 对 index 里已有的条目不追溯。已暂存的大文件要重新 git add 一次。

坑 4:迁移后哈希全变

migrate 重写所有提交 + force push 后,其他位置的旧克隆会分叉。旧克隆要么 git fetch && git reset --hard origin/master,要么重新克隆。还好 standalone 模式下对象都在本地磁盘,操作成本只是几秒钟。

实战:处理远程已重写历史的 LFS 仓库

问题场景

在另一台机器上完成了 LFS 迁移并 force push 后,本地工作副本出现分支偏离:

$ git status
位于分支 master
您的分支和 'smyl/master' 出现了偏离,
并且分别有 13 和 16 处不同的提交。

本地 master 还保留着原始的大文件(mp4/jpg 是完整二进制),而远程 smyl/master 已经是 LFS 指针。189 个文件与远程不同。

解决步骤

1. 同步远程分支

git fetch smyl
git reset --hard smyl/master

2. 处理 LFS 认证问题

如果遇到 git-lfs-authenticate: command not found 错误,说明 SSH 认证链路有问题。临时跳过 smudge 过滤器:

git lfs install --skip-smudge
git reset --hard smyl/master

3. 恢复 LFS 文件

重置后文件会变成 LFS 指针(几十字节的文本文件)。检查本地缓存中有哪些对象可用:

find .git/lfs/objects -type f | wc -l    # 查看本地缓存对象数量
git lfs ls-files -l | grep <oid前缀>     # 查看对象对应哪个文件

恢复可用的文件:

git lfs checkout -- pictures/VID_20260927_100423.mp4  # 恢复单个文件
git lfs checkout                                       # 恢复所有本地可用文件

4. 验证恢复结果

file ./pictures/VID_20260927_100423.mp4
# 输出:ISO Media, MP4 v2 [ISO 14496-14]  ✓ 实际文件
# 而不是:ASCII text                       ✗ 仍然是指针

关键发现

总结