Repository navigation
Cache Git LFS objects - #595
Merged
Merged
Conversation
This was referenced Oct 25, 2026
git-lfs finds the LFS API under the rewritten remote, so clones through /git/ already asked the proxy for LFS objects and got 404s. The proxy now serves the batch API for downloads: it forwards the batch to the upstream, which decides which objects the repository has, and points each download at the proxy. Objects are served from the artifact cache. A miss asks the upstream batch API for a fresh download URL, fetches it and checks it against its sha256 object ID before caching it. Download URLs often name another host than the Git server, such as object storage. The proxy fetches from whatever host the upstream names, with the usual address checks; upstream.auth credentials stay scoped to their hosts. Objects are cached per repository. Uploads and locking are refused. Closes git-pkgs#517.
- Keep signed download URLs out of logs and the database: the fetch uses the signed URL, everything else the URL without its query. - Answer a batch from the cache when the upstream can't be reached or fails, so git-lfs can use cached objects during an outage. Refusals are still passed on. - Answer a HEAD for an uncached object from the batch response instead of downloading it.
- Keep the download URLs of a batch response in memory until shortly before they expire, so the GET that follows needs no second batch request. A kept URL that fails is replaced by a fresh one. - Cut off a download that isn't the object's size. - Copy an object whose content is already stored, such as for a fork, instead of downloading it, once the upstream has confirmed the repository has it. Artifacts are indexed by content hash for this.
andrew
force-pushed
the
feat/git-lfs-cache
branch
from
October 25, 2026 18:20
d3865c7 to
bcda24e
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #517.
git-lfs finds the LFS API under the rewritten remote, at
/git/{name}/{repo}.git/info/lfs, so clones through the proxy already asked it for LFS objects and got 404s. The proxy now serves:POST .../info/lfs/objects/batchfor downloads. The batch goes to the upstream, which decides which objects the repository has, and each download is pointed at.../info/lfs/objects/{oid}/{size}withauthenticated: trueand none of the upstream's headers. Upstream errors per object are passed on, and objects the client didn't ask for are dropped.GET .../info/lfs/objects/{oid}/{size}from the artifact cache (ecosystemgit, name{name}/{repo}, version the OID), so the S3/GCS/R2 backends,DirectServeand retention apply. On a miss the proxy uses the download URL from the client's batch, kept in memory until shortly before it expires, so the GET needs no second batch request. Without one, or when a kept URL fails, it asks the upstream batch API for a fresh one. It fetches with the headers the upstream gave, cuts off a download that isn't the object's size, and checks the content against the OID before caching it. A HEAD on a miss is answered from the batch response without downloading.artifacts.content_hashgets an index (migration 017) for the lookup.Download
hrefs are usually signed. The proxy fetches the signed URL but logs and records only the URL without its query, also in fetch errors.For the open question in the issue, the proxy fetches from any
hrefthe batch response returns:upstream.authcredentials only go to the hosts they're configured for.A per-upstream allowlist of object hosts could follow as an option if someone needs it.
Objects are cached per repository, so having an object cached for one repository doesn't serve it for another. Uploads, locking, other transfers and hash algorithms are refused with LFS error documents.
I also looked at saracen/lfscache. It avoids the second batch request on a miss by handing the original
hrefand headers to the client in signed headers. I didn't do that, because those headers can carry tokens the upstream issued for the proxy'supstream.authcredentials, and thehrefs expire anyway.Tests are in
internal/handler/git_lfs_test.go: a batch API whose hrefs point at a separate object store that needs the batch's header; served from the cache with both down; a batch answered from the cache when the upstream fails; no signature in logs or the artifact record; HEAD without a download; no second batch after the client's, and a fresh URL when the kept one fails; kept URLs expiring; a download of the wrong size cut off; a copy for a fork, and none for a repository the upstream says lacks the object; a tampered object is refused and not cached; refusals.I also ran it end to end: git-lfs 3 in an Alpine container cloned
hf-internal-testing/tiny-random-gpt2from huggingface.co twice through the proxy. Its LFS hrefs point at Hugging Face's CDN. The first clone fetched three objects from the CDN, the second got them from the cache, andgit lfs fsckpassed both times. A third clone with the upstream unreachable got Git and LFS from the cache.