0%

Git LFS 实战笔记

最近项目组很多新项目都使用到了 Git LFS, 解决了 Git 大文件提交容易导致仓库尺寸爆炸的问题.

Git LFS: 简单来说就是用来存储源码仓大文件的, 会把大文件放在"别的地方"存储, 保持源码仓的 Size.

有必要学习一下基础用法.

安装

这个东西可以看成是 Git 的一个扩展插件, 一般安装比较新的 Git 之后, 都自带这个插件.

可以使用命令查看 Lfs 版本:

git lfs version

如果执行后报错 git:'lfs' 不是一个 git 命令, 说明本机 没装 Git LFS, 或装了但没进 PATH. Git LFS 不是 Git 自带的子命令, 需要单独安装:

系统 安装命令
macOS brew install git-lfs
Ubuntu/Debian sudo apt-get install git-lfs
CentOS/RHEL sudo yum install git-lfs
Windows 重装 Git for Windows, 勾选 Git LFS

装完初始化一次即可, 看到 Git LFS initialized. / git-lfs/3.x.x ... 就说明好了:

git lfs install
git lfs version

使用

  1. 启用 LFS 功能
git lfs install

这个命令单个用户只需要执行一次, 后续提交都会自动使用 LFS 功能.

  1. 将文件向 LFS 提交
git lfs track path/file.bin

这个操作会修改 .gitattributes 文件, 这个文件记录了 LFS 管理文件的信息.
文件路径也可以使用通配符 * 来匹配多个文件.

  1. 提交文件
git add .gitattributes path/file.bin
git commit -m "add file.bin"

需要同时提交 .gitattributes 和 二进制文件, 否则 LFS 无法识别.

  1. 推送到远程仓库
git push origin master
  1. 其他参考
    使用命令 git lfs, 想知道的都有写:
git lfs help

也可以去官网瞅瞅: https://git-lfs.com/

把"已经提交过"的大文件 LFS 化

上面 git lfs track 有个容易踩的坑: 它只对之后的提交生效, 已经在历史里的旧 blob 不会自动转成 LFS 指针.

想把仓库里早就存在的大文件(一堆 .bin.zip)一次性转 LFS, 核心命令是 git lfs migrate import, 它会重写提交历史, 把匹配到的文件替换成 LFS 指针.

标准流程

# 1. 先镜像备份, 别省 —— 迁移会改写 commit SHA
git clone --mirror <repo-url> backup.git

# 2. 盘点: 哪些大文件占空间, 命中了多少个
git lfs migrate info --everything --include="*.bin,*.img,*.so,*.zip"

# 3. 真正迁移 (多个类型写在同一个 --include, 用逗号分隔)
git lfs migrate import --everything --include="*.bin,*.img,*.so,*.zip"

# 4. 验证
git lfs ls-files
git lfs migrate info --everything

# 5. 清理本地旧对象
git reflog expire --expire-unreachable=now --all
git gc --prune=now

# 6. 强推远程 (改写历史, 需通知协作者重新 clone)
git push --force --all
git push --force --tags

几个关键点:

  • --include 只能给一个, 多个模式用逗号拼一起, 别写成多个 --include.
  • --everything 覆盖所有分支和标签, 更彻底但更慢; 只想动当前分支就换成 --include-ref=main.
  • 这一步改写了 SHA, 协作者本地仓库会和远程分叉, 必须重新 clone 或 reset --hard.

只迁某个目录, 还带子目录

实战里最常见的是 AOSP 这类大仓: 只把某个目录下的 APK(还嵌套子目录)转 LFS, 不想误伤别处文件. 这时用路径 glob, 而不是只按后缀:

# 先确认能命中
git lfs migrate info --everything --include="device/byteos/base/apks/**/*.apk"

# 命中无误后再 import
git lfs migrate import --everything --include="device/byteos/base/apks/**/*.apk"

**/*.apk 能匹配 apks/ 下任意层级子目录的 APK, 只写 *.apk 只能匹配一层. 成功后 .gitattributes 里应出现:

device/byteos/base/apks/**/*.apk filter=lfs diff=lfs merge=lfs -text

migrate 的几个坑(踩过的都懂)

现象 1: .gitattributes 还是空的, git lfs ls-files 没输出, 仓库毫无变化

日志里有一行 Rewriting commits: 0/0, 这就是答案: 这次迁移一个提交都没改写, 自然什么都没改, 也不会往 .gitattributes 写规则.

最常见原因是 info 加了 --everything 能看到文件, 但 import 没加 --everything. import 不加它时只扫当前 HEAD 可达的提交链; 如果这些大文件是在别的分支/标签上加的, 或当前分支没包含那些提交, 就会匹配不到:

# 正确: info 和 import 的扫描范围要一致
git lfs migrate import --everything --include="device/byteos/base/apks/**/*.apk"

现象 2: migrate info 始终是空的

说明这些文件当前不在任何被扫描的提交里. 先用普通 Git 确认它们在不在历史:

git log --all --oneline -- "device/byteos/base/apks/**/*.apk" | head

如果这里也没输出, 说明文件根本不在 Git 历史里(可能被 .gitignore 忽略、从未被 track), LFS 迁移当然无效.

排查清单: glob 路径大小写/空格/软链接对不上、文件被 .gitignore 忽略、文件只在别的分支、仓库是 shallow/partial clone、上次 migrate 失败留下脏状态.

现象 3: git lfs ls-files 在大仓里很慢

正常现象, 它要扫当前分支所有 tracked 文件挑 LFS 指针. 想快点就加 grep 缩小到目标目录:

git lfs ls-files | grep "device/byteos/base/apks/"

大仓里 migrate 慢得离谱, 怎么提速

AOSP 这种超大仓, migrate import --everything 要遍历所有分支/标签/历史, 慢是必然的. 可以这样优化:

# 1. 别一上来就 --everything, 先只处理当前分支
git lfs migrate import --include-ref=refs/heads/main --include="device/byteos/base/apks/**/*.apk"

# 2. 调高 LFS 并发 (网络好可以试 32 / 64)
git config lfs.concurrenttransfers 16

# 3. 给 Git 本身加性能开关(对大仓遍历、索引加载有帮助)
git config core.preloadindex true
git config core.ignoreStat true
git config core.useBloomFilters true

# 4. 先估算要迁的文件数量, 心里有底
git rev-list --objects --all \
  | git cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)' \
  | grep 'blob' | grep 'device/byteos/base/apks/' | grep '\.apk$' | wc -l

如果已经卡在 migrate import 里: 新开终端 ps aux | grep "git-lfs migrate", CPU/IO 一直在动就让它跑完; 长时间完全不动才考虑中断, 改用 --include-ref 缩小范围重来.

不想重写历史 / 不强推远程, 怎么办

migrate import 会改写 commit SHA、需要强推, 多人协作时风险很大. 如果只想"从现在起走 LFS", 不动旧历史, 有更稳的做法.

做法 A: 只 track, 以后新提交走 LFS(最省事)

git lfs track "device/byteos/base/apks/**/*.apk"
git add .gitattributes
git commit -m "track apks with git lfs"

代价: 历史里的旧文件还是普通 blob, 仓库体积不会立刻下降; 但从现在起新增/修改的匹配文件都会走 LFS. 不需要强推.

做法 B: --no-rewrite —— 在当前分支加一个新提交转指针

# 1. 先加规则并提交
git lfs track "device/byteos/base/apks/**/*.apk"
git add .gitattributes
git commit -m "track apks with git lfs"

# 2. --no-rewrite 生成新提交(把匹配文件转指针), 不动旧提交
git lfs migrate import --no-rewrite -m "migrate apks to git lfs" device/byteos/base/apks/**/*.apk

# 3. 验证后普通推送(不必 force)
git lfs ls-files | grep "device/byteos/base/apks/"
git push origin HEAD

--no-rewrite 的关键: 它在当前分支新建一个提交把匹配文件转成 LFS 指针, 不重写旧提交树, 因此不用 --force, 也不会让协作者分叉. 代价同样是旧历史里的 blob 不会瘦身.

三条路一句话总结:

  • 想瘦历史 → migrate import --everything + 强推(改写 SHA, 通知全员重新 clone);
  • 想不动历史、不强推 → track + migrate import --no-rewrite(只让当前/未来文件走 LFS);
  • 只想以后新文件走 LFS → 光 track 一条规则就够.

证书问题

LFS 内部使用的 HTTP 协议操作大文件, 上传下载什么的会访问源码仓的 API. 一些仓库证书是自签的, 会报证书错误.

Remote "origin" does not support the LFS locking API. Consider disabling it with:
  $ git config lfs.https://xxxx/org/project.git/info/lfs.locksverify false
Uploading LFS objects:   0% (0/1), 0 B | 0 B/s, done
batch response: Post https://xxxx/org/project.git/info/lfs/objects/batch: x509: certificate signed by unknown authority
error: failed to push some refs to 'ssh://git@xxxx/org/project.git.git'

可以通过配置不验证证书来绕过:

# 单个项目配置
git config http.sslVerify false
# 全局配置
git config --global http.sslVerify false

参考