最近在帮团队把二进制产物、安装包和设计稿纳入代码仓库管理,选型上绕不开Git LFS。我们代码审核用的是Gerrit,远程仓库地址是HTTPS形式,配置完git lfs install后我满心以为git push就能把几百MB的大文件推上去。结果第一次git lfs push就让我卡了整整一个下午:终端不停提示输入HTTPS密码,输完Gerrit账号密码没有任何反应,又开始第二轮提示。
那个下午我做了很多蠢事:改Gerrit密码、重装Git LFS、清空钥匙串、让同事把密码发我逐一试……直到我打开GIT_CURL_VERBOSE看到请求URL,才发现一个扎心的事实:我的LFS客户端压根不是在跟Gerrit要密码,它是在向Gerrit的HTTP端口发送LFS对象请求,而那里根本没有LFS服务。最后落地的方案是给这套Gerrit环境单独挂一个lfs-test-server,所有大文件对象走这个独立服务的Basic Auth,Gerrit只保存LFS指针文件。这篇文章就是把整套配置和排查过程整理出来,尤其适合用Gerrit做代码审核、又要管理大文件,并且被“git lfs push总是提示输入https密码”烦到崩溃的团队。
1. Gerrit里的大文件难题,为什么非得再挂一个lfs-test-server
1.1 Git LFS到底在做什么:指针入库,对象另存
Git LFS的全称是Git Large File Storage,它并不是Git官方内置功能,而是一个基于Git过滤器机制的外部扩展。它通过clean和smudge两个过滤器与Git核心协作:当你执行git add时,LFS的clean过滤器会把真实文件替换成一个很小的文本指针文件;当你执行git checkout时,smudge过滤器会读取指针文件并从LFS服务器把真实内容拉回来。
指针文件长这样:
code复制version https://git-lfs.github.com/spec/v1
oid sha256:4d7a214f6f8c8b6f0b1d3d8e5c0a3f8b0f2f0b6e0d1e3a0c1f9d8e7f6a5b4c3d2e1f
size 12345678
所以整个仓库的Git对象体积不会因为大文件而膨胀,真正的大文件被存到独立的LFS对象存储里。LFS客户端与服务器的通信遵循一套Batch API:上传对象前,客户端先询问服务器“这些对象你有哪些,哪些我需要传”,服务器给出回应后,客户端再逐个上传。这个设计的好处是断点续传和去重都能交给服务器处理,但也意味着:一旦LFS端点配置错了,你的所有上传请求都会打到错误的地方,认证自然也会跟着错。
1.2 Gerrit自带LFS支持,但“支持”不等于“适合你的场景”
Gerrit从2.15版本开始内置了LFS相关插件,理论上你可以在Gerrit的HTTP端口直接处理LFS请求,对象也会落在Gerrit站点目录下。这套方案在单机小规模场景下确实能跑,但它有几个实际约束。
第一,对象与Gerrit仓库存储在同一个文件系统,大文件多了以后,备份、迁移、垃圾回收都绕不开Gerrit站点本身。第二,Gerrit的LFS插件和权限模型绑定得比较深,你很难在不想开放Gerrit账号的前提下让CI机器拉取对象。第三,Gerrit在评审流里只接受指针文件的push,如果LFS对象和指针都走同一个地址,评审被拒后对象清理也是个麻烦。
所以,当团队需要把大文件对象和代码评审平台解耦时,外部LFS服务是更清晰的架构。lfs-test-server正是Git LFS官方早期用来测试协议实现的轻量服务,一个二进制文件,启动一个端口就能提供完整的LFS Batch API和对象存取能力。它的价值在于“轻”,也在于“标准”:所有Git LFS客户端都能直接对接它。
这里回应一个经常被问到的问题:Git LFS会不会某天合并进Git主线?我的看法是大概率不会直接合并。Git本身通过filter机制已经给LFS留好了接口,LFS作为外部工具长期演进反而是更健康的模式。近年Git提出的partial clone、promisor remote实际上也在解决类似问题,但Gerrit这套偏传统的审核体系里,LFS依然是兼容性最好的大文件方案。
1.3 这套架构里的两条通道,是理解密码问题的前提
我们的目标架构可以用一句话说清楚:小指针走Gerrit,大对象走lfs-test-server。
开发者在本地提交后,git push会同时做两件事。第一件事,把仓库里的LFS指针提交和普通代码提交一起推送到Gerrit的refs/for审核分支;第二件事,git-lfs客户端会单独发起Batch API请求,把大文件对象推送到lfs-test-server。评审人在Gerrit上点击合入后,本地执行git lfs pull,从lfs-test-server拉取实际对象。
问题就在这里:这是两条完全独立的通道,两个完全独立的认证体系。Gerrit的SSH密钥和HTTP Basic Auth管的是代码通道;lfs-test-server的-user/-pass管的是对象通道。很多人碰到“git lfs push总是提示输入https密码”,就是因为客户端把对象请求发给了Gerrit,然后又把Gerrit的账号密码拿去问lfs-test-server要权限,两边谁也不认谁,只能一遍遍弹密码框。这个根因我们留到第3章详细拆。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境准备:从Gerrit配置到lfs-test-server启动
2.1 客户端安装Git LFS,别在版本上栽跟头
客户端安装我以macOS为例,因为本地开发机大部分是Mac:
bash复制brew install git-lfs
git lfs install
Linux直接用包管理器,比如Debian系用apt install git-lfs,CentOS系用yum install git-lfs;Windows建议从Git LFS官网下载安装包,或者用Scoop执行scoop install git-lfs。
git lfs install这步很关键,它会在你的~/.gitconfig里注册filter.lfs.clean和filter.lfs.smudge命令,并添加对应的merge驱动。如果你发现git lfs命令能执行,但add大文件后git仓库里还是看到原始文件而没有指针化,99%是这步没做,或者项目环境变量GIT_LFS_SKIP_SMUDGE影响到了本地。
版本方面我只有一个建议:尽量用新版。早期git-lfs对Batch API的Accept头兼容有问题,和lfs-test-server这类实现不严谨的服务端配合时,会出现各种诡异报错,包括但不限于missing object和batch response: invalid status。目前我本地稳定跑的是2.x以上版本,功能完整,对git lfs env的输出也更友好。
2.2 Gerrit侧开通:SSH密钥、审核分支和仓库权限
如果你还没有配置过Gerrit的SSH密钥,这一步建议先做。Gerrit默认的SSH端口是29418,很多人新装环境会忘记在防火墙放行这个端口。密钥生成和上传很简单:
bash复制ssh-keygen -t ed25519 -C "your-email@example.com" -f ~/.ssh/id_ed25519_gerrit
把生成的id_ed25519_gerrit.pub内容复制到Gerrit页面右上角Settings -> SSH Keys里。之后建议在~/.ssh/config里写上Host别名,省得每次记端口:
code复制Host gerrit
HostName gerrit.example.com
Port 29418
User yourname
IdentityFile ~/.ssh/id_ed25519_gerrit
在Gerrit上创建项目时,比如config-service,需要注意权限模型。Gerrit的默认权限下,普通开发者只能push到refs/for/*,也就是说你的提交会进入评审队列,而不是直接落在master上。这对代码审核是好事,但对LFS指针文件也一样。执行推送的时候请使用:
bash复制git push origin HEAD:refs/for/master
如果你只是想快速验证LFS联动,可以在项目权限里给某个用户组配置refs/*的Push权限,让它绕过评审直接推送,但生产环境我不建议这么做。
2.3 启动lfs-test-server:一行命令和一个数据目录
lfs-test-server的安装方式,最省事的是去GitHub的git-lfs/lfs-test-server仓库直接下载对应平台的release二进制,官方也支持用Go编译。我习惯放在/opt/lfs-server下,数据目录放在/data/lfs-content:
bash复制cd /opt/lfs-server
lfs-test-server -listen :8080 -host lfs.example.com -scheme http -user lfsuser -pass 'change-me-strong' -dir /data/lfs-content
解释一下几个参数:
-listen:服务监听地址,我这里是所有网卡的8080端口。-host:客户端访问时的对外主机名,这个值会写进服务器返回的响应里,别设成localhost,否则其他机器拿不到正确地址。-scheme:告诉客户端当前是http还是https,配合反向代理时务必设置正确。-user和-pass:管理员账号,也是后续所有LFS客户端要使用的Basic Auth口令。-dir:对象和元数据存储目录。
如果你希望长期跑,可以用systemd或supervisor托管。我本地测试时则直接后台运行,用nohup也行。启动后可以用简单请求确认服务活着:
bash复制curl -i -u lfsuser:'change-me-strong' http://localhost:8080/info/lfs/objects/batch
如果返回401或JSON错误都算正常,关键是服务有响应;如果连接被拒绝,先查端口有没有起来,再查防火墙。服务器端的日志会记录每次请求的路径和状态码,后面排查密码问题时,它是最好的旁证。
3. 密码提示问题的完整排查链路:不是记错密码,是打错了门
3.1 现场取证:用GIT_TRACE和curl看请求到底打到哪
先说现象。配置好一切后,我第一次执行:
bash复制git lfs push --all origin
终端立刻弹出:
code复制Username for 'https://gerrit.example.com':
Password for 'https://yourname@gerrit.example.com':
我输入正确的Gerrit账号密码后,没有出现熟悉的Uploading LFS objects进度条,而是再次弹出同样的用户名密码提示,来回三四次之后才报错退出。这种“循环要求认证”的体验,比直接报Access denied还让人抓狂。
排查时我做了两件事。第一件,给git加上详细日志环境变量重新跑:
bash复制GIT_TRACE=1 GIT_CURL_VERBOSE=1 git lfs push --all origin 2>&1 | tee /tmp/lfs-trace.log
日志里能看到curl层发出的每个HTTP请求。第二件,用一个最简单的请求直接访问本地LFS服务的Batch API:
bash复制curl -v http://localhost:8080/info/lfs/objects/batch
两边一对照,问题立刻清晰:我本地git-lfs客户端实际上在访问https://gerrit.example.com/config-service/info/lfs/objects/batch,也就是它把远程仓库HTTPS地址当成了LFS端点,请求发给了Gerrit的HTTP服务。而Gerrit那边显然没有对应的LFS处理逻辑,或者说返回了认证挑战,于是git在终端循环弹窗。
这个现象可以整理成一张判断表:
| 异常现象 | 可能原因 | 快速验证方式 |
|---|---|---|
| 一直弹HTTPS密码框,但输完又弹 | LFS请求打到了Gerrit HTTP端口,未指向LFS服务 | GIT_CURL_VERBOSE看请求URL |
| 密码框只弹一次,但报401 Access denied | LFS服务地址对了,但账号密码不对 | 直接curl访问Batch API看状态码 |
| 弹密码框,且curl报SSL证书错误 | HTTPS自签名证书未被信任 | 临时用GIT_SSL_NO_VERIFY测试 |
3.2 根因:客户端没有把LFS端点指向lfs-test-server
我的远程仓库URL是https://gerrit.example.com/config-service.git,git-lfs的默认行为是:如果仓库没有显式配置lfs.url,它就顺着remote地址去推断LFS端点,最终拼接出远程仓库所在路径下的info/lfs。这意味着你remote指向Gerrit,LFS对象请求就必然发给Gerrit。
要让大文件对象真正被lfs-test-server接收,必须在仓库里显式声明LFS端点。推荐在仓库根目录创建.lfsconfig文件并提交到版本库,这样所有协作者克隆后都能自动使用同一套端点:
code复制[lfs]
url = http://lfs.example.com:8080
如果你不想让某个LFS端点配置对所有协作者生效,或者想临时覆盖,可以只改当前仓库的.git/config:
bash复制git config lfs.url http://lfs.example.com:8080
这里有一个关键点需要强调:.lfsconfig里不要写任何用户名密码。常见错误是有人图省事写http://lfsuser:change-me-strong@lfs.example.com:8080,结果换密码、换人、或URL里含特殊字符时全都出问题。凭据应该交给git的credential机制管理,而不是写进文件。
把端点改到lfs-test-server后,再执行git lfs push --all origin,终端终于出现了Uploading LFS objects: 100% (1/1, 120 MB)的完整进度。此时再去lfs-test-server的数据目录看,对象文件已经落盘。
3.3 第二个坑:两个认证体系,别拿Gerrit的密码去敲lfs-test-server的门
端点指对了,密码框确实不再循环弹了,但有的人会死在下一步:lfs-test-server返回了401,git又弹了一次密码框。
原因不复杂:Gerrit的HTTP认证和lfs-test-server的Basic Auth是两套独立账号体系。你在Gerrit页面设置的密码是Gerrit的登录凭证,而lfs-test-server只认启动时配置的-user和-pass,或者你在它内部用户表里创建的账号。拿Gerrit的密码去请求lfs-test-server,服务器当然不认。
解决办法也简单:在客户端明确告诉git,访问http://lfs.example.com:8080这个地址时用哪个用户名密码。有两种常见姿势。
第一种,使用git credential helper,让git在首次认证后把凭据缓存起来。macOS上推荐用osxkeychain:
bash复制git config --global credential.helper osxkeychain
Linux上如果只是临时测试,可以用cache:
bash复制git config --global credential.helper 'cache --timeout=3600'
第二种,如果你希望完全不被交互式弹窗打扰,在CI或自动化脚本里通常会借助http.extraHeader直接注入Authorization头。先将用户名密码做Base64编码:
bash复制echo -n 'lfsuser:change-me-strong' | base64
然后配置:
bash复制git config --global http.http://lfs.example.com:8080/.extraHeader "Authorization: Basic bH...=="
注意这种方式的凭据会以明文形式出现在git配置里,只适合一次性调试或CI专用账号。如果你在本地开发,我更推荐用credential helper,既安全又省心。
3.4 第三个坑:自签名证书同样会触发循环认证
还有一种比较隐蔽的情况,会让“总是提示输入https密码”这个问题在修好端点后依然出现:lfs-test-server通过HTTPS对外提供服务,但用的是内网自签名证书,git在每次请求时因证书验证失败而无法完成认证循环,最后也表现为一遍遍要密码。
判断方法很简单,先看curl:
bash复制curl -v https://lfs.example.com:8080/info/lfs/objects/batch
如果curl报self signed certificate之类的错误,而git配置里又没有信任这个CA,LFS请求就会在TLS层失败。网上很多人会建议直接git config --global http.sslVerify false,我不推荐在生产环境这么干。正确做法是把自建CA证书导入系统信任链,或者针对特定域名用http.sslCAInfo指定CA文件:
bash复制git config --global http.https://lfs.example.com/.sslCAInfo /etc/ssl/certs/internal-ca.crt
这样既保留了TLS校验,又让LFS服务器能被正常访问。在配完证书之后再跑一次git lfs push,你会发现终端终于安安静静地出现进度条。
3.5 验证修复:用git lfs env把环境一次看穿
排查完上述几个点,最有效的验证工具其实是git lfs env。在仓库目录下运行:
bash复制git lfs env
它会打印出当前仓库的LFS端点、过滤器状态、SSH远程、.lfsconfig与.gitconfig的合并结果等关键信息。你会看到类似这样的输出:
code复制Endpoint=https://gerrit.example.com/config-service.git/info/lfs (auth=basic)
LocalWorkingDir=...
如果Endpoint那一行显示的仍然是Gerrit地址,说明.lfsconfig没有生效,请检查文件是否在仓库根目录、文件名是否拼错。如果显示的是http://lfs.example.com:8080,就说明LFS端点正确。之后再结合实际push结果,就能确认密码问题是否彻底解决。
4. 配置清单与推送验证:从“还在弹密码”到“一次通过”
4.1 直接抄作业的配置速查表
这一套配置踩完坑之后,我整理了一份速查表,团队新成员照着配即可:
| 配置项 | 配置位置 | 值示例 | 说明 |
|---|---|---|---|
| Git LFS客户端 | 开发机全局 | brew install git-lfs + git lfs install |
注册clean/smudge过滤器 |
| Gerrit SSH | ~/.ssh/config |
见2.2 | 代码通道建议用SSH,省去HTTP密码 |
| 仓库remote | .git/config |
ssh://yourname@gerrit.example.com:29418/config-service |
推送指针文件走SSH |
| LFS端点 | 仓库根目录.lfsconfig |
http://lfs.example.com:8080 |
所有协作者共享 |
| LFS账号 | 开发机credential helper | lfsuser / change-me-strong |
只负责对象通道 |
| CA证书 | git http.sslCAInfo |
/etc/ssl/certs/internal-ca.crt |
仅HTTPS时必需 |
这里再提醒一句:.lfsconfig提交到Gerrit仓库之前,先确认它里面没有敏感信息。否则任何一个能clone这个仓库的人,都能看到你的LFS密码。如果确实需要给不同环境设置不同端点,建议用git config的includeIf机制按路径区分,别把密码硬编码进仓库。
4.2 用一个100MB安装包跑通全流程
我习惯用真实大小的文件做测试,因为小文件根本暴露不出问题。假设仓库里放一个dist/app-1.0.0.bin,约120MB:
bash复制git lfs track "dist/*.bin"
git add dist/app-1.0.0.bin
git commit -m "chore: add distribution package"
git push origin HEAD:refs/for/master
第一次push时,你会看到git-lfs先做对象上传:
code复制Uploading LFS objects: 100% (1/1, 120 MB)
随后git才开始推送代码提交和指针文件。这里的顺序很重要:LFS对象上传成功之后,指针文件才会被推送到Gerrit。如果LFS对象上传失败,整个push会被中断,不会出现“代码进去了,对象没进去”的中间状态。
评审人侧验证更简单:
bash复制git fetch origin
git checkout change-branch
git lfs pull
git lfs pull会扫描当前工作区里的LFS指针,并从lfs-test-server拉取真实文件。我建议在评审人机器上执行git lfs status看当前哪些文件是LFS对象,再用git lfs ls-files列出仓库里已跟踪的LFS文件,避免拿到一堆空指针文件而不自知。
4.3 评审流下的push与孤儿对象问题
在Gerrit工作流里,开发者的提交先到refs/for/master,评审通过后由Gerrit合入master。这个过程中LFS对象其实在评审之前就已经上传到lfs-test-server了。换句话说,即使评审没通过,大文件已经在对象服务器里占了一份空间。
这引出一个lfs-test-server的天然短板:没有垃圾回收机制。被拒的change、反复修改后废弃的提交,都会在服务器上留下孤儿对象。解决办法没有特别优雅的,我目前是写一个定时任务,通过lfs-test-server的元数据接口导出对象列表,再和Gerrit仓库中所有历史指针的oid集合做对比,筛出超过30天没有被任何指针引用的对象手动清理。这种脚本不复杂,但如果你完全没有备份意识,千万别贸然删对象目录。
5. 这套组合在生产化之前,你要知道的事
5.1 lfs-test-server的边界:能测试,也要有敬畏
lfs-test-server这个名字已经说明了它的定位:官方早期用来测试协议的服务器。它支持基本的Batch API、对象存取和管理员账号,但它没有高可用、没有内置认证插件、没有对象生命周期管理,存储也只是一台机器的本地目录。如果你只是几个人的团队、网络环境可控、对象总量不大,它确实够用;一旦涉及几十个人的持续集成、对象量上TB,或者有跨机房容灾要求,就必须考虑迁移到商业化LFS存储或自建MinIO这类带标准S3接口的方案。
另一个操心的点是备份。lfs-test-server的-dir目录里既有对象文件,也有元数据。我见过有人只备份了Gerrit仓库目录,LFS对象服务器挂掉后,所有历史大文件全军覆没。Gerrit里还能看到指针文件,但真实内容永远找不回来。我的建议是:把-dir纳入和其他重要数据同样的备份策略,并且定期在另一台机器上做一次git lfs fetch --all验证恢复链路。
5.2 CICD流程里拉取LFS对象的正确姿势
团队把CICD跑起来后,另一个高频问题浮现:Jenkins或GitLab Runner在构建时需要拉取LFS对象,但它并不知道lfs-test-server的账号密码。我在实际项目里用了两种方式。
第一种,在CI节点上提前配置git credential helper为store,并把LFS服务器的凭据写入该节点用户的~/.git-credentials文件。之后CI脚本正常执行git lfs pull就不会弹密码框。这种方式只适合隔离的构建网络,因为凭据是明文存在CI节点上的。
第二种,如果构建任务根本不需要LFS大文件,比如只是做代码静态检查,那就在CI脚本的最前面设置:
bash复制export GIT_LFS_SKIP_SMUDGE=1
这样clone和checkout时git-lfs不会尝试拉取对象,构建速度会快很多。这里要注意:构建产物如果依赖这些大文件,跳过smudge会导致文件内容是空指针文本,构建大概率失败。所以这个开关只适合明确不需要LFS内容的job使用。
顺便说一句很多人问的GitLab和Gerrit在CICD里的分工。我们目前的流程是:GitLab做开发主仓库和CI入口,Gerrit做合入master前的评审网关,LFS对象统一使用同一个中心LFS服务。这样无论代码走到哪个平台,大文件都只有一份存储,不会出现GitLab一份、Gerrit一份的割裂。实际操作上,只要保证两个平台上的仓库都配置了相同的lfs.url,这套模型就能成立。
5.3 关于“Git LFS会不会并入Git主线”的个人看法
这个热词每隔一段时间就会出现。每次有人讨论“Git官方是不是要内置LFS了”,我的回答都比较平淡:Git不会也不需要把LFS整进核心。原因很简单:Git的filter机制本身就是一个通用扩展点,LFS只是这个大文件场景最成功的实现。强行把某种对象存储协议塞进Git核心,反而会让核心变重。
更值得关注的是Git原生的partial clone和promisor remote。它们让Git可以不下载完整历史也能正常工作,思路和LFS“懒加载”有些相似。但对于Gerrit这种以评审为中心的代码审核系统,原生partial clone的支持并不完善,至少在目前的版本里,LFS仍然是最省心的选择。我的建议是:别再等“并入主线”,把LFS端点、权限模型、对象存储生命周期这几件事规划好,比什么都实在。
这套“Gerrit + lfs-test-server”的组合,我实际跑了半年多。如果只说一条经验,那就是:遇到git lfs push反复要密码的问题,先让git lfs env说话,而不是先怀疑密码。八成以上的情况是LFS端点没有指到真正要接收大文件的服务,剩下两成才是凭据持久化或自签名证书的锅。配置上把.lfsconfig提交进仓库、用SSH通道走代码、凭证交给credential helper,这套模式基本能让你不再被密码框打扰。如果你的团队规模再大一点,尽早把lfs-test-server替换成更成熟的对象存储,代码层不用动,只换lfs.url和凭据,迁移成本远比你想象的低。
