Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/git/vole.git

Write over SSH with a registered key:

git clone ssh://git@0.0.0.0: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 · 17d 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/server/server.go +13−1

@@ -15,6 +15,7 @@ import (
15 15 "github.com/rodent-software/vole/internal/config"
16 16 "github.com/rodent-software/vole/internal/identity"
17 17 "github.com/rodent-software/vole/internal/index"
18 "github.com/rodent-software/vole/internal/lfs"
18 19 "github.com/rodent-software/vole/internal/repo"
19 20 "github.com/rodent-software/vole/internal/runs"
20 21 "github.com/rodent-software/vole/internal/signing"
@@ -31,6 +32,7 @@ type Server struct {
31 32 Identity *identity.Store
32 33 Runs *runs.Store
33 34 Index *index.Store
35 LFS *lfs.Store
34 36 Proxy *transport.Proxy
35 37 SSH *sshserver.Server
36 38 Key *signing.Key
@@ -76,9 +78,12 @@ func New(cfg *config.Config, binPath string, log *slog.Logger) (*Server, error)
76 78 Open: rs.Open,
77 79 Log: log,
78 80 })
81 lfsStore := lfs.New(cfg.LFSDir(), cfg.LFSObjectMaxBytes)
79 82 ssh, err := sshserver.New(sshserver.Config{
80 83 Identity: ident,
81 84 Repos: rs,
85 LFS: lfsStore,
86 LFSWarnBytes: cfg.LFSWarnBytes,
82 87 Log: log,
83 88 HostKeyPath: cfg.SSHHostKey(),
84 89 MaxBlobBytes: cfg.MaxBlobBytes,
@@ -95,10 +100,11 @@ func New(cfg *config.Config, binPath string, log *slog.Logger) (*Server, error)
95 100 if err != nil {
96 101 return nil, err
97 102 }
103 wp.LFS = lfsStore
98 104 wp.Logger = func(format string, args ...any) { log.Warn(fmt.Sprintf(format, args...)) }
99 105
100 106 s := &Server{
101 Cfg: cfg, Repos: rs, Identity: ident, Runs: runStore, Index: ix,
107 Cfg: cfg, Repos: rs, Identity: ident, Runs: runStore, Index: ix, LFS: lfsStore,
102 108 Proxy: proxy, SSH: ssh, Key: key, Web: wp, Log: log,
103 109 }
104 110 s.mux = s.routes()
@@ -126,6 +132,7 @@ func (s *Server) routes() *http.ServeMux {
126 132 mux.HandleFunc("GET /r/{name}/issues/{id}", w.IssueView)
127 133 mux.HandleFunc("GET /r/{name}/requests", w.Requests)
128 134 mux.HandleFunc("GET /r/{name}/requests/{id}", w.RequestView)
135 mux.HandleFunc("GET /r/{name}/runs/{id}/log", w.RunLog)
129 136
130 137 mux.HandleFunc("GET /healthz", func(rw http.ResponseWriter, r *http.Request) {
131 138 rw.Header().Set("Content-Type", "text/plain")
@@ -134,6 +141,11 @@ func (s *Server) routes() *http.ServeMux {
134 141
135 142 // Anonymous, read-only git. Registered without a method so git's POSTs reach it.
136 143 mux.Handle("/git/", s.Proxy)
144 // LFS reads share the same read rule and the same URLs as clone; uploads are
145 // refused here and happen over SSH.
146 mux.HandleFunc("POST /git/{name}/info/lfs/objects/batch", s.LFSBatch)
147 mux.HandleFunc("GET /git/{name}/info/lfs/objects/{oid}", s.LFSObject)
148 mux.HandleFunc("HEAD /git/{name}/info/lfs/objects/{oid}", s.LFSObject)
137 149 return mux
138 150 }
139 151