1. 故障现场:一条推送失败信息引发的迁移中断
先说结论:Git LFS(Large File Storage)的推送失败,本质上不是因为"网络不好"或"权限不对"这种表面原因,而是你的仓库里那些大文件对象和Git主仓库的引用关系出了断层。我这次迁移GitLab仓库时踩的坑,前前后后折腾了快一个下午,最后定位到根因时,才发现问题出在一个特别容易忽略的配置项上。
先说下背景。公司有个自建GitLab实例,跑了快三年,里面沉淀了不少项目。最近因为服务器资源要重新规划,需要把整个GitLab从旧实例迁移到新实例。这里说一下,GitLab官方推荐的迁移方式有两种:一种是直接做实例级迁移(用backup/restore或者migration工具),另一种就是逐仓库地通过git clone --mirror加git push --mirror来做。我这次选择的是后者,因为源实例版本比较老,直接做实例级备份恢复可能遇到兼容性问题,逐仓库迁移反而更可控。
仓库本身不大,源码部分加起来也就几百MB,但有一个仓库例外——里面塞了不少设计稿PSD文件、视频素材和数据集,全部通过Git LFS管理。这就意味着迁移这个仓库时,不能只推常规的Git对象,还得把LFS对象也一并推到新GitLab的LFS存储里。
我当时的操作大致是这样的:
# 在旧实例上做镜像克隆 git clone --mirror git@old-gitlab.example.com:group/project.git # 进入裸仓库,添加新实例的远程地址 cd project.git git remote add new-origin git@new-gitlab.example.com:group/project.git # 推送所有引用 git push new-origin --mirror前面几步都很顺利,refs都推过去了,分支、标签都同步到了新实例。但问题出在最后一步——当GitLab检测到仓库里有LFS指针文件,自动触发LFS对象校验和推送时,终端直接抛出了错误:
Uploading LFS objects: 0% (0/40), 0 B | 0 B/s, done. LFS: Put "https://new-gitlab.example.com/group/project.git/info/lfs/objects/batch": git-lfs: command not found我当时一看这个报错,第一反应是"新实例上没装git-lfs客户端",但转念一想,不对,我是在本地执行推送,走的是SSH协议,真正在服务端执行的git-lfs命令应该由新GitLab实例自己处理。后面我又试了直接指定LFS端点、清理本地缓存、重建裸仓库,折腾了一段时间才真正找到原因。这篇博文就把完整排查过程写出来,希望能帮到同样卡在"迁移带LFS的Git仓库"这个问题上的朋友。
2. 重新认识Git LFS:为什么迁移普通仓库的经验在这里全失效
2.1 LFS把"一个仓库"拆成了"两个存储系统"
很多第一次接触LFS的人会把它想象成"Git的一个扩展功能,让大文件也能正常提交"。这个理解方向是对的,但不够精确。准确地说,Git LFS把原本单一的Git仓库拆成了两个存储系统:一个是常规的Git对象库,存的是源码、文本、配置这类小文件;另一个是LFS对象存储,专门存放那些超过阈值的大文件。
两者通过一个"指针文件"建立关联。也就是说,你在工作目录里看到的那个10MB的PSD文件,实际提交到Git仓库里的并不是文件本身,而是一个只有几百字节的文本文件,里面记录了文件的哈希值和大小。真正的二进制内容被存储在.git/lfs/objects目录下,在推送时通过LFS的批量API上传到远程。
这个设计带来的好处是显而易见的:Git仓库的体积不会随着二进制文件的增多而膨胀,克隆速度也快得多。但它同时带来了一个迁移时的大坑——迁移一个带LFS的仓库,必须保证"Git引用"和"LFS对象"两部分都能完整同步到目标端。如果只推了refs而丢了LFS对象,新仓库拉下来之后,那些大文件全是损坏的指针,直接在业务上等于数据丢失。
2.2 LFS指针文件长什么样
为了后面排查方便,这里先展示一下LFS指针文件的结构。你可以在仓库里用git show HEAD:path/to/largefile.psd查看,如果它是LFS管理的文件,输出会是类似这样的:
version https://git-lfs.github.com/spec/v1 oid sha256:2c2b6d4a9a3e5c8e6f73b4a0c8d0a1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d size 10485760其中oid是文件内容SHA-256哈希,size是原始文件大小。远程的LFS存储就是靠这个oid来索引和存放二进制内容的。所以你在迁移时,如果某个LFS对象的oid没能在新实例的LFS存储里找到对应内容,那这个指针文件在语义上就是"悬空"的。
2.3 为什么"普通仓库迁移四步走"在LFS仓库上会失效
平时迁移一个普通的Git仓库,无外乎clone、add remote、push、校验这四步。但LFS仓库多了一个维度:除了Git对象要推,LFS对象也要推。而且两者推送的时机、协议、认证方式都不一样。
Git对象走的是git receive-pack,走SSH或HTTP协议;LFS对象走的是HTTPS协议,通过/info/lfs/objects/batch这个端点进行批量上传或下载。Git LFS客户端在推送时会自动检测仓库里的指针文件,然后尝试把本地/.git/lfs/objects里对应的对象上传到远程。
这里有个很隐蔽的设计:LFS对象的上传不是由git push这个命令直接触发的,而是由Git LFS的pre-push钩子触发的。如果你在迁移时用的是git clone --mirror得到的裸仓库,裸仓库默认是没有钩子的,而且它也没有工作目录、没有/.git/lfs目录,LFS对象实际上根本不在这个裸仓库里。直接对它执行git push --mirror,推上去的只是指针文件——这在远程仓库看来,所有大文件都是损坏状态。
我当时就是在这里栽了跟头:镜像克隆得到的裸仓库又把--mirror推给新实例,逻辑上走了"常规仓库迁移"的老路,但它绕过了LFS对象的真正存放位置,导致推送LFS对象时暴露出一堆问题。
2.4 顺着这个思路,先自查三个"是不是"
如果你也遇到了类似问题,先别急着重装环境或清缓存,按顺序自查三件事:
- 本地原始仓库(非裸仓库)里有没有
.git/lfs/objects目录?如果这个目录是空的,那说明LFS对象要么从没成功拉取过,要么被git lfs prune清理过。 - 远程新GitLab实例有没有正确开启LFS支持?转到新实例的管理后台,检查"设置 -> 通用 -> 可见性与访问权限",看LFS开关是不是开着。部分自建GitLab在初始配置时LFS是默认开启的,但也有小概率被管理员关掉。
- 新实例的LFS存储路径可用性。如果你用的是本地存储而非对象存储(比如S3、MinIO),得确认GitLab的运行用户对存储目录有读写权限。这个在容器化部署或迁移数据盘之后特别容易出问题。
3. 逐段排障:从报错信息到根因定位的完整链路
3.1 错误一:"command not found"到底是谁的命令
回到最开始报的错:
LFS: Put "https://new-gitlab.example.com/group/project.git/info/lfs/objects/batch": git-lfs: command not found这里有两个关键信息。第一,它请求的路径是/info/lfs/objects/batch,这是LFS批量API的标准端点。第二,报错说git-lfs: command not found,但这个"command not found"到底是发生在我的本地机器还是新GitLab服务器上?从报错格式来看,这是本地Git LFS客户端尝试向远程发起HTTPS请求后,由远程返回的错误响应体。
也就是说,问题大概率出在新GitLab实例这一侧。GitLab收到LFS批量请求后,内部需要调用git-lfs可执行文件来处理这个请求,但实例上找不到这个命令,于是返回了错误信息。
为什么会找不到?这里分几种常见情况:
- GitLab是通过Omnibus包安装的,正常情况下会自带
git-lfs,但如果安装时用了精简版或自定义安装路径,有可能漏装。 - GitLab是Docker部署的,而镜像版本比较老,或者镜像被裁剪过,把
git-lfs给裁掉了。 - GitLab版本存在已知bug,导致LFS端点处理异常。这个在13.x和14.x早期版本中不少见。
先不要急着上服务器重新安装,继续往下排查。
3.2 检查新实例的LFS状态:SSH到服务器看细节
我当时的做法是直接SSH登录到新GitLab服务器上,先用git-lfs version确认本机是否安装了Git LFS,再用gitlab-rails runner或者gitlab-rails console检查实例的LFS配置状态。
$ git-lfs version git-lfs/3.2.0 (GitHub; linux amd64; go 1.19)这里没问题,服务器上装了Git LFS。那问题就要进一步收窄——要么是GitLab没有正确调用到git-lfs,要么是请求在到达LFS处理逻辑之前就被拦截了。
接着看配置文件中的LFS开关:
$ sudo gitlab-rails runner "puts ApplicationSetting.current_without_cache.to_json" | jq '.lfs_enabled, .lfs_max_file_size'输出结果:
false 104857600看到lfs_enabled是false的瞬间,我大致就明白是怎么回事了。新实例的管理员当初在初始化配置时,可能是在"可见性与访问权限"设置里手动关掉了LFS,也可能是从旧配置导入时漏了这一项。总之,新实例根本不接受任何LFS对象上传,所以任何形式的大文件推送都会在这道关被拦下来。
3.3 为什么是"push --mirror"而不是"push --all"也会触发LFS检查
再来回答一个小问题:我用的是git push --mirror推送裸仓库,按理说裸仓库里没有/.git/lfs/objects目录,也没有pre-push钩子,为什么最终还是会触发LFS上传?
细看下去会发现,新GitLab实例在接收到包含LFS指针文件的提交时,会主动检查这些指针文件对应的LFS对象是否已经存在。如果不存在,它会在receive过程中返回错误,要求客户端先上传LFS对象。这一步是GitLab服务端的LFS集成逻辑在起作用,和客户端有没有执行git lfs push没有直接关系。
所以,哪怕我用镜像克隆得到的裸仓库推送,只要提交记录里有指针文件,新GitLab就会尝试去LFS存储里找对应的对象。找不到,就要求客户端上传;上传请求发到LFS批量API,又被LFS开关给拦了。这就是错误链路的大致流程。
3.4 解决新实例的LFS开关问题
确认根因后,修复就顺理成章了。到新GitLab的管理后台,或者在服务器上直接用Rails runner修改设置:
$ sudo gitlab-rails runner "ApplicationSetting.current_without_cache.update!(lfs_enabled: true)"执行完之后再确认一遍:
$ sudo gitlab-rails runner "puts ApplicationSetting.current_without_cache.lfs_enabled" true顺带说一句,lfs_max_file_size这个参数也需要看一眼。它的单位是字节,默认值是104857600,也就是100MB。如果你要迁移的仓库里有超过这个体积的单个文件,即使LFS开关开了,上传也会因为超过大小限制而失败。我检查了一下仓库里的PSD素材,最大的一个90MB,没超限,所以这一步不用额外调整。如果你的文件很大,记得先把这个值改大。
3.5 二次推送:又撞上HTTP 422错误
LFS开关打开后,我满怀信心地重新推送,结果又报了个新错误:
LFS: Put "https://new-gitlab.example.com/group/project.git/info/lfs/objects/batch": 422这里需要用GIT_TRACE=1来定位更详细的信息:
$ GIT_TRACE=1 GIT_TRACE_CURL=1 git push new-origin --mirror 2>&1 | tee /tmp/push.log在日志里能看到批量API的请求体:
{ "operation": "upload", "transfers": ["basic"], "objects": [ {"oid": "2c2b6d4a9a3e5c8e6f73b4a0c8d0a1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d", "size": 10485760} ] }422错误表示请求格式正确但语义上无法处理。考虑到LFS开关已经打开,我继续在服务器上看GitLab的生产日志:
$ sudo gitlab-ctl tail gitlab-rails/production.log | grep -A 10 lfs日志里出现了一条关键记录:
LFS storage path does not exist or not writable: /var/opt/gitlab/git-data/lfs-objects到这里,真正的第二个坑浮出水面:新实例的LFS存储目录没创建,或者GitLab进程没有权限写入。
3.6 修掉存储目录的坑
这个问题的背景是:新实例的数据盘迁移时,只挂载了Git仓库的主存储路径,而LFS对象存储目录/var/opt/gitlab/git-data/lfs-objects没有被一并迁移或预热。GitLab启动时如果没检测到这个目录,不会主动创建,后续写入时就会报422。
在服务器上执行:
$ sudo mkdir -p /var/opt/gitlab/git-data/lfs-objects $ sudo chown git:git /var/opt/gitlab/git-data/lfs-objects $ sudo chmod 1770 /var/opt/gitlab/git-data/lfs-objects然后用sudo gitlab-ctl reconfigure重新加载配置,再sudo gitlab-ctl restart gitlab-workhorse和sudo gitlab-ctl restart puma重启相关服务。重新验证后,新实例的LFS批量API已经能正常响应了。
4. 修复LFS推送的三种方案:按场景选型,不要盲目套
一旦把服务端问题修好,接下来要考虑的是"客户端该用什么方式把LFS对象推上去"。这里其实不止一种解法,取决于你手里有什么"牌"。我这次同时实验了三条路径,按推荐程度排个序。
4.1 方案A:从原始工作仓库(而非裸仓库)发起推送
这是我最推荐的方案,也是最终成功迁移的做法。前提是你本地还保留着原始的非裸仓库,它的.git/lfs/objects目录里应该有完整的LFS对象缓存。
操作思路很简单:不要在镜像克隆的裸仓库上做推送,而是回到原始仓库,设置好新实例的remote,然后让Git LFS客户端通过pre-push钩子自动上传LFS对象,再推refs。具体步骤:
cd /path/to/original-repo # 检查本地LFS对象缓存是否完整 git lfs fsck --check # 添加新实例的remote地址 git remote add new-origin git@new-gitlab.example.com:group/project.git # 先把LFS对象推送上去 git lfs push new-origin --all # 再推送全部分支和标签(非镜像模式) git push new-origin --all git push new-origin --tagsgit lfs push只会把LFS对象推送到远程LFS存储,不会推普通Git引用。这个操作的意义在于,把"推LFS对象"和"推Git引用"解耦开,先让远程LFS存储把该收的文件收齐,再推引用,这样引用推送时就不会因为服务端校验LFS对象缺失而失败。
4.2 方案B:裸仓库场景下手动指定LFS对象路径
如果你已经没有原始工作仓库了,只有当时镜像克隆出来的裸仓库,也不要慌。你可以让裸仓库"借"本地其他仓库的LFS对象缓存来用。Git LFS提供了一个环境变量GIT_LFS_SKIP_SMUDGE,还有lfs.storage配置项,可以指定LFS对象的存储路径。
举个例子,假设你本地还有一个完整的工作仓库在/data/backup/original-repo,它的LFS对象都在/data/backup/original-repo/.git/lfs/objects。你可以这样操作裸仓库:
cd /path/to/project.git # 告诉Git LFS去借用其他仓库的缓存 git config lfs.storage /data/backup/original-repo/.git/lfs git lfs push new-origin --all但这里有个前提:裸仓库本身要能识别出所有需要推送的LFS对象。Git LFS在推送时会遍历所有refs里的指针文件,找出所有oid,然后从lfs.storage指向的目录里找对应文件。如果对象缺失,它照样会报错。所以方案B适合"缓存完整但仓库形态是裸仓库"的场景。
4.3 方案C:把缺失的LFS对象手动打包上传
还有一种更极端的情况:你手里既没有完整的工作仓库,裸仓库的缓存也不完整。那只能从旧实例上把LFS对象拉下来。
先克隆旧实例仓库,但要注意,不要用--mirror,而是用普通克隆,并跳过LFS文件的自动下载(smudge)。这里有点绕,LFS的机制默认会在git checkout时自动下载所有被LFS管理的大文件,如果你只是想要对象本身,这一步可能会拉取大量重复数据。用GIT_LFS_SKIP_SMUDGE=1可以避免自动下载,但这样你又拿不到LFS对象了。
更靠谱的做法是:
cd /path/to/backup-repo # 克隆旧实例上的仓库(普通克隆,工作区和缓存都会创建) git clone git@old-gitlab.example.com:group/project.git cd project # 强制git-lfs下载当前分支内所有LFS对象 git lfs pull执行完这一步后,.git/lfs/objects目录下应该有了所有当前分支引用到的LFS对象。然后再走方案A的推送流程。这个方法相对繁琐,但胜在能在不访问服务器存储的情况下,把对象从旧实例完整搬出来。我这次因为旧实例还活着,前两个方案又都遇到过缓存缺失的插曲,最终实际是用方案A配合一次git lfs pull把缺的对象补齐然后推送成功的。
4.4 方案选型对比
| 场景 | 推荐方案 | 核心命令 | 注意事项 |
|---|---|---|---|
| 有完整原始工作仓库 | A | git lfs fsck --check+git lfs push new-origin --all | 先做完整性检查,缺失就git lfs pull |
| 只有裸仓库但其他仓库有缓存 | B | git config lfs.storage <path>+git lfs push new-origin --all | 裸仓库需要识别出全部oid,必要时手动遍历refs |
| 本地无任何缓存 | C | git clone+git lfs pull+git lfs push | 拉取时用普通克隆,确保lfs对象被完整检出 |
| 对象过多、体积极大 | A+C | git lfs migrate export后走HTTP批量上传 | 也可以直接导出到独立存储,再在新实例侧导入 |
提示:无论选哪种方案,推送前都建议先在新实例上建一个空的同名单仓库(
group/project.git),保证LFS批量API的仓库上下文是正确的。如果你推送到一个不存在的仓库,GitLab会返回404或Project Not Found,那就连LFS上传的入口都进不去。
5. 迁移完成之后的收尾工作:不检查等于白迁
5.1 验证引用完整性和LFS对象完整性
迁移后第一件事,就是在本地克隆一份新仓库,确认所有大文件能正常检出。推荐用普通克隆(注意这里的clone会触发LFS smudge自动下载):
$ cd /tmp $ git clone git@new-gitlab.example.com:group/project.git verify-repo $ cd verify-repo $ git lfs fsck --checkgit lfs fsck会检查当前工作区中所有LFS指针文件对应的对象是否存在、哈希是否匹配。如果它输出类似OK或者没有明确报错,说明LFS对象完整性没问题。
接着检查分支和标签。用以下命令对比新旧实例的引用是否一致:
# 旧实例 $ git ls-remote git@old-gitlab.example.com:group/project.git | sort > /tmp/old-refs.txt # 新实例 $ git ls-remote git@new-gitlab.example.com:group/project.git | sort > /tmp/new-refs.txt # 比对 $ diff /tmp/old-refs.txt /tmp/new-refs.txt如果没有差异,说明分支和标签级别没有缺口。对于MR(Merge Request)数据,因为自建GitLab的MR是存在数据库里的,这种逐仓库迁移天然会丢掉MR记录,需要在迁移前导出或让开发团队知悉,这是另外一个话题,先按下不表。
5.2 处理.gitattributes的失效问题
还有一个容易被忽视的细节:如果项目里的.gitattributes文件定义了LFS规则(例如*.psd filter=lfs diff=lfs merge=lfs -text),但迁移后的仓库里这个文件本身没有一起提交,或者提交中丢失了这部分内容,那么新检出的文件可能不会被当成LFS文件处理。
我这次就遇到了一个类似的坑:新仓库虽然LFS对象都在,但克隆后PSD文件直接以原始二进制形式留在工作区了,没有走LFS的smudge流程。查了一圈才发现,是当时用--mirror推送时,引用里有些分支的.gitattributes内容没有包含LFS规则。解决方法是把最新分支上的.gitattributes文件重新提交一次,推送新提交后再次验证。
建议你迁移后专门跑一下:
# 列出所有LFS追踪的文件扩展名 $ git lfs track # 检查当前分支上有没有文件被LFS正确管理 $ git lfs ls-filesgit lfs ls-files会列出当前分支所有被LFS管理的文件,如果输出的文件列表和旧实例一致,说明规则和指针都完整。
5.3 旧实例的LFS缓存别急着清理
迁移完成后,很多人会立马清理旧实例上的仓库和LFS对象存储,给服务器腾空间。我的建议是:至少保留一周再清理,最好等到新实例跑过一轮完整的备份之后再清。
原因很简单,LFS对象是内容寻址存储(content-addressable storage),它的特点是哈希相同内容唯一,一旦误删,无法通过Git的引用历史恢复。旧实例的LFS对象存储就是一份天然的备份,保留它能应对任何"迁移后发现某分支还有未推送的LFS对象"这种突发事件。
6. 一些更隐蔽的坑:实例版本、HTTP端点、批量上传限流
6.1 新老实例的GitLab版本差异会带来兼容性问题
我这次源实例是GitLab 14.2,新实例是15.11。两者之间LFS批量API的版本差异其实不算大,基本是向后兼容的。但如果你的源实例非常老(比如12.x甚至11.x),LFS对象可能存储的还是旧格式的/info/lfs/objects端点,而新版本GitLab对这个端点的处理逻辑有过调整。这种跨大版本迁移,最稳妥的方式是先升级源实例到接近目标版本的版本,再做仓库迁移。
另外,GitLab从12.x开始集成了LFS的"验证对象真实性"机制,旧实例上如果有一些损坏或缺失的LFS对象,迁移时可能直接因为校验失败而中断。这种情况在日志里通常表现为Invalid LFS object或Object is corrupt。排查时需要到/var/opt/gitlab/gitlab-rails/shared/lfs-objects目录下按oid的前两个字符分目录查找文件,确认文件大小和哈希。
6.2 HTTP 401和认证方式导致的上传失败
另一种容易遇到的问题,是LFS批量API的认证失败。Git LFS客户端默认会在发送批量请求时携带Basic Auth认证头,而这个认证对应的用户、token必须和Git推送时使用的认证信息配套。
如果你在推送时设置remote地址为https://username:password@new-gitlab.example.com/group/project.git,LFS客户端会把这个URL里的用户名密码解析出来用于认证。但如果你用的是SSH地址推Git引用,而LFS请求被重定向到HTTPS端点,认证又会走SSH密钥转换逻辑,这时候可能出现一个现象:Git引用推送成功,但LFS上传全部401。这种情况可以通过显式配置lfs.url来解决:
$ git config lfs.url https://new-gitlab.example.com/group/project.git/info/lfs6.3 批量上传的并发和限流问题
如果仓库里的LFS对象非常多(几千个甚至上万个),第一次git lfs push会因为并发数太低而奇慢无比。Git LFS默认并发是3,可以在推送前修改本地的并发数配置:
$ git config lfs.concurrenttransfers 8另外一个隐形限制是GitLab侧对LFS对象体积的硬限制。尽管管理员能配置lfs_max_file_size,但如果新实例使用了对象存储(比如MinIO或S3),对象存储自己的分片上传大小和并发限制也要纳入考虑。网络环境比较复杂的时候,可以改用git lfs push --object-id手动指定已缓存的LFS对象id进行补传,避免每次失败都从头开始。
我这次实际迁移时,40个LFS对象里大约有10个在上传时出现过假死,调整并发数之后,剩余所有对象都在十几分钟内传完了。
7. 回到最初的问题:为什么"推送LFS失败"会变成"不能迁移仓库"
记一次排障,最终的价值不在于解决了某一次具体的报错,而在于想明白了问题背后的逻辑链条。我再把这个链条重新串一遍,方便你以后遇到类似问题时能快速对照:
LFS仓库的迁移本质上是两个层的迁移:引用层(refs)和对象层(LFS objects)。引用层迁移可以通过标准Git命令完成,但对象层的迁移依赖LFS批量API,而这个API依赖新实例的LFS配置和存储目录。任何一环出了问题,整个迁移就会中止——表现出来的现象就是"Git push失败"其实只是表象,"LFS对象无法上传"才是根因。
我这次排障用到的排查顺序,简单总结就是:
- 确认新实例的
lfs_enabled配置是否为true; - 确认新实例的LFS存储目录
/var/opt/gitlab/git-data/lfs-objects是否存在且可写; - 确认本地LFS对象缓存是否完整,用
git lfs fsck --check检查; - 确认LFS对象的批量上传是否真的触发了,用
GIT_TRACE=1看请求日志; - 确认远程端点的认证、大小限制、并发数是否在合理范围内。
顺着这个链路走一遍,绝大多数"推送LFS失败"的问题都能定位到具体的环节,而不是停留在一个含糊的报错信息上瞎试。特别提醒一点,如果你在企业环境里用的是HTTP代理或内网HTTPS证书,还要额外检查Git LFS客户端是否信任了这个证书,否则批量API请求很可能在TLS握手阶段就被掐断了。
最后说个我实际操作中的体会:迁移带LFS的仓库,永远不要只盯着"能push成功"这个目标,要盯"push之后仓库能完整clone出来且大文件可用"这个终态。判断一次迁移是否成功,不是在源仓库里执行完推送命令那一刻,而是在新实例上从零克隆出仓库、git lfs fsck无报错、所有二进制文件都能正常打开的那一瞬间。我这次就是因为前期只关注了"推引用有没有成功",忽略了LFS对象层,才多走了一两个小时弯路。希望看到这篇记录的你,能少踩一次同样的坑。