记一次已推送仓库启用 Git LFS 的完整迁移与验证过程

yaml 复制代码
title: 记一次已推送仓库启用 Git LFS 的完整迁移与验证过程
type: knowledge
status: evergreen
tags:
  - Git
  - Git LFS
  - 仓库治理
  - 版本控制
created: 2026-03-24
updated: 2026-03-24
aliases: []

记一次已推送仓库启用 Git LFS 的完整迁移与验证过程

摘要

这篇笔记记录了一次已推送仓库启用 Git LFS 的完整迁移过程:一个已经推送到 GitHub 的 Obsidian 中文知识库,因为图片附件较多而开始考虑 Git LFS。重点在于梳理历史重写、远端覆盖和 force push 风险之间的取舍,以及如何验证迁移结果已经落实到远端历史。

背景

这次处理的对象不是代码构建项目,而是一个已经推送到 GitHub 的 Obsidian 中文知识库。随着内容持续积累,仓库体积越来越大,压力主要集中在 99_assets/ 里的图片附件上。

如果这是一个多人协作、分支频繁交错的仓库,我不会轻易对已经推送的历史做重写;但这次仓库主要由我单人使用,协作边界清晰,因此可以接受改写历史和后续 force push 的成本。本文按评估、迁移、验证、收尾四个阶段展开,便于后续复用这条操作链路。

先评估,而不是直接改

这次没有直接执行 git lfs trackgit lfs migrate import,而是先确认仓库现状,再判断是否值得做历史迁移。迁移前的几个关键事实如下:

  • 仓库总大小约为 59M
  • .git 中的 pack 大小约为 25.24 MiB
  • 99_assets/ 下约有 205 个文件
  • 最大的图片文件大多落在约 0.4--1.3 MB 这个区间
  • 仓库当时没有 .gitattributes
  • 仓库当时还没有使用 Git LFS

评估命令如下。这里沿用命令输出中的单位表示;59M25.24 MiB0.4--1.3 MB 分别来自不同命令的输出口径,仅用于描述量级:

bash 复制代码
du -sh "/vault"
git -C "/vault" count-objects -vH
find "/vault/99_assets" -type f | wc -l
find "/vault/99_assets" -type f -printf '%s %p\n' | sort -nr | head
ls -l "/vault/.gitattributes"
git lfs ls-files

这些观测说明:仓库总体量级不算夸张,但 99_assets/ 已有 205 个文件,附件规模已经稳定;同时仓库中没有 .gitattributesgit lfs ls-files 也没有输出,说明当时尚未启用 Git LFS。是否值得做历史迁移,就基于这几个事实判断。

方案选择:为什么最后选"迁移历史"

在真正动手之前,我先比较了三种可选路径:

方案 优点 代价 最终结论
只对未来新增文件启用 LFS 改动最小,不需要重写已有历史 旧历史里的大文件仍然保留在 Git 对象中,历史体积问题不会真正缓解
将现有历史一起迁移到 LFS 未来新增文件和历史中的二进制资产都能统一管理 需要重写历史,并承担 force push 和本地旧引用清理成本
暂不处理 当前没有任何操作风险 仓库会继续按原方式增长,问题不会消失

最后选择第 2 种,不只是为了控制未来增长,更是为了统一历史中的二进制资产管理方式。如果只对未来新增文件启用 LFS,那么旧提交里的图片仍然会继续留在原有 Git 历史里,仓库的历史负担并没有真正解决。

这次之所以能接受"迁移现有历史"这一成本更高的方案,也和仓库的使用边界直接相关:它主要由单人维护,历史重写带来的协作成本相对可控,不会波及一个已经稳定协作的多人团队。在这个前提下,历史迁移的收益高于 force push 的代价,因此最终决定把 *.png*.jpg*.jpeg*.gif*.webp*.pdf 一并纳入 Git LFS 管理。涉及历史改写和后续回滚时,我也会联想到 一次完整的 Git 提交撤销与代码恢复经历记一次严重的 Git 分支提交错误与修复复盘 里那些"先保留恢复抓手,再动历史"的原则。

实施前保护:为什么要做双重备份

这里说明的是迁移前的备份策略:本地备份 tag 用于快速回到迁移前状态,bundle 备份用于保留一份独立于当前仓库的完整历史快照。

这次使用的标识分别是 backup/pre-lfs-migration-2026-03-24/vault-pre-lfs-2026-03-24.bundle

lua 复制代码
git -C "/vault" tag "backup/pre-lfs-migration-2026-03-24"
git -C "/vault" bundle create "/vault-pre-lfs-2026-03-24.bundle" --all

关键点只有一个:单独的 tag 会继续钉住旧历史,bundle 才是独立于当前仓库的最终回滚点。即使后面执行了引用清理、reflog expiregit gc --prune=now,只要 bundle 还在,仍然可以脱离当前仓库的引用状态进行恢复。涉及"回滚点应该放在哪里"这个问题时,如何从 Git 提交中恢复被删除的文件 里的恢复思路同样适用。

实施过程:真实步骤与中途问题

首次失败暴露了一个明确前置条件:git lfs migrate import 要求工作区必须是干净的。

先执行 git lfs track,生成 .gitattributes 并写入需要跟踪的文件类型规则:

arduino 复制代码
git -C "/vault" lfs track "*.png" "*.jpg" "*.jpeg" "*.gif" "*.webp" "*.pdf"

执行后,工作区出现了一个未提交的新文件 .gitattributes。第一次执行历史迁移时,命令直接失败,关键输出如下:

sql 复制代码
override changes in your working copy?  All uncommitted changes will be lost! [y/N]
working copy must not be dirty

根因很明确:工作区不干净,未提交的 .gitattributes 阻塞了 git lfs migrate import --everything

失败后,为了恢复 clean working tree,本次实际执行的命令链是先再次创建 bundle 备份,再删除工作区中未提交的 .gitattributes。这里没有重做 tag,因为迁移前创建的 tag 已经存在,继续创建同名 tag 没有额外意义。

lua 复制代码
git -C "/vault" bundle create "/vault-pre-lfs-2026-03-24.bundle" --all
rm "/vault/.gitattributes"

工作区恢复干净后,再次执行历史迁移:

python 复制代码
git -C "/vault" lfs migrate import --everything --include="*.png,*.jpg,*.jpeg,*.gif,*.webp,*.pdf" --object-map="/vault/.git/lfs-migrate-object-map.csv"
git -C "/vault" push --force-with-lease origin main

这一次迁移成功,随后把改写后的 main 强推到远端;实际使用的是 git push --force-with-lease origin main 这一更安全的形式。这个失败过程也暴露了一个容易忽略的前置条件:对已推送仓库做 LFS 历史迁移时,除了考虑 记一次严重的 Git 分支提交错误与修复复盘 中那类历史改写风险,还必须先确保工作区没有未提交阻塞项,否则命令会在真正开始改写前停止。

如何验证迁移真的生效

本地迁移命令成功,并不等于远端历史已经切换完成。验证至少要覆盖四件事:本地已跟踪文件数量、远端规则是否存在、远端文件内容是否已变成 pointer、本地与远端分支状态是否一致。

首先,git lfs ls-files | wc -l 的结果是 205

bash 复制代码
git -C "/vault" lfs ls-files | wc -l

其次,用 git show origin/main:.gitattributes 可以直接看到远端 main 上已经存在 6 条 LFS 规则:

bash 复制代码
git -C "/vault" show origin/main:.gitattributes

还需要再抽样检查一张远端图片文件:

bash 复制代码
git -C "/vault" show "origin/main:99_assets/ElasticSearch 7.6.0 学习笔记/16fc2485335af34de3b8030383c480a8.png" | head -n 3

输出是标准的 LFS pointer:

arduino 复制代码
version https://git-lfs.github.com/spec/v1
oid sha256:54f2710cdf4c130a030b243514d9f7164e5302bf57e18acaa7c285fa0004cf9c
size 48742

出现这种 pointer 形式,才能证明远端存放的已经不是原始二进制内容,而是由 Git LFS 接管后的指针对象。

最后,检查分支状态是否已经收敛:

css 复制代码
git -C "/vault" status --short --branch
git -C "/vault" rev-list --left-right --count origin/main...main

这两条命令的实际结果分别是 ## main...origin/main0 0,说明本地 main 与远端 origin/main 已经没有领先或落后差异。结合前面的检查,可以判断这次迁移已经落实到远端历史。

收尾:为什么还要清理本地旧历史

历史迁移完成后,并不意味着旧对象会立刻自动从本地仓库消失。只要旧引用还在,或者 reflog 里仍然能追到迁移前的对象,Git 就没有理由立刻回收这些内容。因此收尾阶段仍然很重要。

这次收尾动作包括:删除本地备份 tag、删除迁移产生的对象映射文件、保留 bundle 备份,并通过 reflog expiregit gc --prune=now 触发旧引用回收:

lua 复制代码
git -C "/vault" tag -d "backup/pre-lfs-migration-2026-03-24"
rm "/vault/.git/lfs-migrate-object-map.csv"
git -C "/vault" reflog expire --expire=now --all
git -C "/vault" gc --prune=now

这里保留 bundle、删除本地备份 tag,是为了不再让 tag 继续钉住旧历史,同时继续保留一个独立于当前仓库的最终回滚点。

清理前后,.git pack 大小从 25.33 MiB 下降到了 852.13 KiB。这也说明一件事:LFS 历史迁移真正想获得的本地收益,不只是"规则写进去了",而是后续还要把旧引用清理掉,否则旧 pack 体积不会自然消失。

经验总结

这次流程最值得记住的,是几条风险控制原则。

第一,对已推送仓库做历史重写之前,必须先确认协作边界。单人维护的仓库和多人并行开发的仓库,能承受的代价完全不同。

第二,最终回滚点应尽量独立于当前仓库的内部引用体系,因此 bundle 比单纯的 tag 更可靠。

第三,本地迁移成功不等于远端已经切换完成;只有规则、pointer 和分支状态都验证通过,迁移才算真正落地。

第四,git lfs migrate import 对工作区洁净度有严格要求。未提交的 .gitattributes 就足以阻塞整个迁移流程。

这次流程可复用的最小 checklist

  • 评估现状
  • 创建 tag 与 bundle 备份
  • 确认 working tree clean 后执行迁移
  • 验证 LFS 规则、pointer 与分支状态
  • 清理本地旧引用并保留 bundle

相关笔记

相关推荐
大家的林语冰3 小时前
《前端周刊》React 败北,虾皇登基,OpenClaw 勇夺 GitHub 第一开源软件
前端·javascript·github
ShineWinsu5 小时前
对于Linux:git版本控制器和cgdb调试器的解析
linux·c语言·git·gitee·github·调试·cgdb
zhensherlock5 小时前
Protocol Launcher 系列:Microsoft Edge 浏览器唤起的优雅方案
javascript·chrome·microsoft·typescript·edge·github·edge浏览器
嗡嗡嗡qwq6 小时前
【如何使用vscode+github copilot会更加省额度】
vscode·github·copilot
汪海游龙6 小时前
03.25 AI 精选:Wine 11重写内核层提速跑Windows游戏
github
研究点啥好呢7 小时前
3月24日GitHub热门项目推荐|让AI无所不能
人工智能·python·开源·github
Timer@7 小时前
TypeScript + React + GitHub Actions:我是如何打造全自动化 AI 资讯系统的 - 已开源
react.js·typescript·github
badhope7 小时前
Matplotlib实战30例:全类型图表代码库
人工智能·python·plotly·github·matplotlib
badhope7 小时前
最小二乘与最速下降法实战解析
人工智能·机器学习·plotly·github·matplotlib