| 1 | # 0036 — Git LFS objects are stored per repository and served through the API |
| 2 | |
| 3 | **Status:** adopted |
| 4 | |
| 5 | ## Decision |
| 6 | |
| 7 | Ghost speaks the Git LFS batch API and its `basic` transfer under the clone URL, beside git's own routes: |
| 8 | |
| 9 | | Route | Does | |
| 10 | | --- | --- | |
| 11 | | `POST /<owner>/<repo>.git/info/lfs/objects/batch` | Says which objects to upload, or where to download each | |
| 12 | | `PUT /<owner>/<repo>.git/info/lfs/objects/<oid>` | Stores one object | |
| 13 | | `GET /<owner>/<repo>.git/info/lfs/objects/<oid>` | Serves one object | |
| 14 | | `GET`, `POST /<owner>/<repo>.git/info/lfs/locks` |
| 15 | | `POST /<owner>/<repo>.git/info/lfs/locks/verify` | The pusher's locks and everyone else's | |
| 16 | | `POST /<owner>/<repo>.git/info/lfs/locks/<id>/unlock` | Unlocks: the owner, or an admin with ` |
| 17 | |
| 18 | `GitBasicAuthMiddleware` authorizes them as it does git: an upload or a lock change needs write access, a download or a lock list read access. LFS bodies are parsed before it runs, since the batch's operation is in its body. Each action carries the batch request's own `Authorization` header, so the client needs no second credential lookup. Hrefs start with ` |
| 19 | |
| 20 | Objects live at `lfs/<repositoryId>/<oid>`, one `lfs_object` row each. Only SHA-256 oids are accepted. An upload follows the release asset reservation (0025): the row is inserted under the account's lock with an id of its own, the bytes stream to |
| 21 | |
| 22 | LFS objects count against the `lfs` limit, except in forks, where they count against the `fork` limit like everything else a fork holds (0035). |
| 23 | |
| 24 | Forking copies the parent's objects, rows and bytes, inside the fork's transaction, and bills them with the fork. Merging a pull request from another repository copies the objects named by pointer files in the head's own commits (`mergeBase..head`) that the base lacks: the bytes before the merge's transaction, the rows inside it, billed to the base like the merge. Deleting a repository deletes its objects by prefix before the rows go. |
| 25 | |
| 26 | Over SSH, `git-lfs-authenticate <repo> <operation>` answers with the HTTPS endpoint and a `Bearer` token signed with |
| 27 | |
| 28 | Locks are rows in `lfs_lock`, one per repository and path, listed in path order with the path as the cursor. They are advisory, as on GitHub: git-lfs checks them before a push, and the server does not refuse a push that touches a locked path. |
| 29 | |
| 30 | The blob and raw endpoints serve a pointer file as its object when the repository holds it. The blob response's `lfs` field says `stored`, |
| 31 | |
| 32 | ## Why |
| 33 | |
| 34 | Per-repository copies keep access the same as git's: an object is readable exactly where its repository is. A shared, content-addressed store would save the copy on fork, but would need reference counting before anything could be deleted, and would let anyone who knows an oid claim it without uploading. |
| 35 | |
| 36 | Bytes go through the API rather than presigned URLs for the reason 0025 gives: a self-hosted bucket is often reachable only from the API's network. |
| 37 | |
| 38 | ## Consequences |
| 39 | |
| 40 | - A fork copies every LFS object its parent holds, on the request that creates it. |
| 41 | - A merge copies only what the head repository holds. An object the author never pushed stays missing in the base too. |
| 42 | - A merge that fails after copying leaves the copied objects behind, orphans as in 0010. |
| 43 | - `git-lfs-transfer`, the pure-SSH protocol, is refused; git-lfs falls back to `git-lfs-authenticate` |
| 44 | - A token cannot be revoked before it expires; removing the user's SSH key or access stops new ones, and access is still checked on every request. |
| 45 | - An upload that finishes while its repository is being deleted can leave its object behind, an orphan as in 0010. |
| 46 | - The web UI shows an LFS file as its pointer only when the repository does not hold its object. |