vole
Clone
Read only, no credentials:
https://rodent.software/vole.git
Write over SSH with a registered key:
git clone ssh://git@rodent.software:2222/vole.git
A free and open-source forge that lives in your Git repo.
Phase 5: LFS objects, over SSH for writes and the proxy for reads
64ac5071a173e4e9ce8bc8d7170f9b0411bee52a · Keenan Nemetz · 18d ago · parent 35eaeccf
Phase 5: LFS objects, over SSH for writes and the proxy for reads Large files leave the git object database and land in one content-addressed store per instance. Reads are anonymous like clone; writes ride the SSH key that already authenticates the push, so no token exists anywhere. - internal/lfs: sha256-addressed store that verifies content and size before an object becomes visible; parser for the pointer format a client writes - HTTP: POST .../info/lfs/objects/batch for downloads, GET .../objects/<oid> with the object id as ETag; upload over HTTP is refused and names SSH - SSH: git-lfs-transfer speaking the pure SSH protocol (pkt-line), so `git lfs push` needs nothing but the key at the handshake - pre-receive: a pointer whose object is missing, or whose size disagrees with the stored object, fails the push that introduced it; a binary over lfs_warn_bytes is accepted with a `git lfs track` suggestion. Checks run before the protected-branch gate, because an admin's approval bypass is about review, not about whether the commit can be checked out - runs: a finished log becomes a content-addressed object in the same write that records the status, served at /r/<repo>/runs/<id>/log - vole gc [--dry-run] [--grace]: marks pointer targets from every ref in every repository plus archived logs, sweeps the rest. Mark-and-sweep rather than reference counts: a counter that is one too low deletes an artifact, a walk over the refs cannot be wrong about what is reachable
internal/lfs/batch.go +60
| @@ -0,0 +1,60 @@ | |||
| 1 | package lfs | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "encoding/json" | ||
| 5 | "fmt" | ||
| 6 | "net/http" | ||
| 7 | ) | ||
| 8 | |||
| 9 | // The JSON bodies of POST .../info/lfs/objects/batch. Only the fields Vole | ||
| 10 | // reads or writes are modelled; git-lfs sends more (transfers, ref, hash_algo) | ||
| 11 | // and ignores unknown keys in the response. | ||
| 12 | |||
| 13 | type BatchRequest struct { | ||
| 14 | Operation string `json:"operation"` | ||
| 15 | Transfers []string `json:"transfers,omitempty"` | ||
| 16 | HashAlgo string `json:"hash_algo,omitempty"` | ||
| 17 | Objects []Object `json:"objects"` | ||
| 18 | } | ||
| 19 | |||
| 20 | type Object struct { | ||
| 21 | OID string `json:"oid"` | ||
| 22 | Size int64 `json:"size"` | ||
| 23 | } | ||
| 24 | |||
| 25 | type BatchResponse struct { | ||
| 26 | Transfer string `json:"transfer,omitempty"` | ||
| 27 | HashAlgo string `json:"hash_algo,omitempty"` | ||
| 28 | Objects []ObjectResponse `json:"objects"` | ||
| 29 | } | ||
| 30 | |||
| 31 | type ObjectResponse struct { | ||
| 32 | OID string `json:"oid"` | ||
| 33 | Size int64 `json:"size"` | ||
| 34 | Actions map[string]Action `json:"actions,omitempty"` | ||
| 35 | Error *ObjectError `json:"error,omitempty"` | ||
| 36 | } | ||
| 37 | |||
| 38 | type Action struct { | ||
| 39 | Href string `json:"href"` | ||
| 40 | Header map[string]string `json:"header,omitempty"` | ||
| 41 | ExpiresIn int `json:"expires_in,omitempty"` | ||
| 42 | } | ||
| 43 | |||
| 44 | type ObjectError struct { | ||
| 45 | Code int `json:"code"` | ||
| 46 | Message string `json:"message"` | ||
| 47 | } | ||
| 48 | |||
| 49 | // ErrorBody is the top-level error git-lfs shows the user. | ||
| 50 | type ErrorBody struct { | ||
| 51 | Message string `json:"message"` | ||
| 52 | } | ||
| 53 | |||
| 54 | // WriteError replies with the media type git-lfs expects, so the reason reaches | ||
| 55 | // the user instead of "unexpected content type". | ||
| 56 | func WriteError(w http.ResponseWriter, status int, format string, args ...any) { | ||
| 57 | w.Header().Set("Content-Type", MediaType) | ||
| 58 | w.WriteHeader(status) | ||
| 59 | json.NewEncoder(w).Encode(ErrorBody{Message: fmt.Sprintf(format, args...)}) | ||
| 60 | } | ||