给本地裸仓库启用 Git LFS:1.6GB 媒体历史迁移实录
十月 01, 2026 [杂项] #Git #迁移背景
一个生日视频剪辑项目,仓库里堆了近 200 个手机拍摄的照片/视频,仓库结构是"本地裸仓库做远端":
- 工作副本:
~/project/birthday(编辑、提交) - 裸仓库:
~/project/repos/birthday(git init --bare的产物,作为origin接收 push)
问题:历史里的 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 GiB | 123.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 ✗ 仍然是指针
关键发现
- 本地缓存有限:只有迁移前就存在的 LFS 对象会留在本地缓存中
- 指针 vs 实际文件:
file命令可以快速区分——ASCII text是指针,ISO Media/JPEG image是实际文件 - 按需恢复:
git lfs checkout只恢复本地缓存中已有的对象,缺失的对象需要git lfs pull - 认证链路:SSH 方式需要服务器安装
git-lfs ≥ 3.0,否则会报git-lfs-authenticate: command not found
总结
- 本地路径/SSH 裸仓库用 LFS 的门槛极低:客户端装 git-lfs(≥3.0),远端零配置或只装个包
- 存量历史用
git lfs migrate import --everything一步迁移,配合两边gc --prune=now,git 对象库从 1.60 GiB 降到 123 KiB - 迁移前把工作区收拾干净(.gitignore + renormalize 提交),躲开 migrate 的 dirty 校验
- 裸仓库目录现在就是完整备份(git 历史 +
lfs/objects/全量对象),直接打包即可迁移 - 日常 add/commit/push 完全无感,需要省磁盘时
git lfs prune清本地缓存 - 远程已重写历史时,用
git reset --hard <remote>/<branch>同步,配合git lfs checkout恢复本地可用文件