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 6 slice 1: the tree is the site
d3296cda0c18f78097e37373822292707113d245 · Keenan Nemetz · 17d ago · parent 64ac5071
Phase 6 slice 1: the tree is the site A repository's `pages` branch is served at <repo>.<pages_domain>, straight out of git. No export step, no build to drift: the push moves the branch and the next request reads the new tree. - internal/pages: a pool of `git cat-file --batch` processes per active repository (closed after two idle minutes), an LRU keyed by (commit, path), and tree parsing in git's binary format with the object-id width detected rather than assumed - ETag is the blob sha, Last-Modified the commit time, Content-Type from the extension or a sniff; If-None-Match and If-Modified-Since answer 304 - directories serve index.html and redirect to add the trailing slash; path resolution walks trees, so a traversal-shaped request is a missing page - repository and ACL are resolved once per one-second memo window together with the branch tip, which is what makes the read path fast: doing it per request spawned three git processes and cost 3.8 ms, against 6.7 µs warm - sites follow clone visibility, so a private repository answers 404; the repo page links to the site when pages_domain is configured Measured: a site pushed over SSH is live in 1.4 s (the phase criterion is 5 s); warm requests 6.7 µs, cold 144 µs. Custom domains and their TLS certificates are the remaining Phase 6 work.
internal/server/server.go +30−1
| @@ -16,6 +16,7 @@ import ( | |||
| 16 | 16 | "github.com/rodent-software/vole/internal/identity" | |
| 17 | 17 | "github.com/rodent-software/vole/internal/index" | |
| 18 | 18 | "github.com/rodent-software/vole/internal/lfs" | |
| 19 | "github.com/rodent-software/vole/internal/pages" | ||
| 19 | 20 | "github.com/rodent-software/vole/internal/repo" | |
| 20 | 21 | "github.com/rodent-software/vole/internal/runs" | |
| 21 | 22 | "github.com/rodent-software/vole/internal/signing" | |
| @@ -33,6 +34,7 @@ type Server struct { | |||
| 33 | 34 | Runs *runs.Store | |
| 34 | 35 | Index *index.Store | |
| 35 | 36 | LFS *lfs.Store | |
| 37 | Pages *pages.Server | ||
| 36 | 38 | Proxy *transport.Proxy | |
| 37 | 39 | SSH *sshserver.Server | |
| 38 | 40 | Key *signing.Key | |
| @@ -107,6 +109,17 @@ func New(cfg *config.Config, binPath string, log *slog.Logger) (*Server, error) | |||
| 107 | 109 | Cfg: cfg, Repos: rs, Identity: ident, Runs: runStore, Index: ix, LFS: lfsStore, | |
| 108 | 110 | Proxy: proxy, SSH: ssh, Key: key, Web: wp, Log: log, | |
| 109 | 111 | } | |
| 112 | // Sites are opt-in: without a pages domain there is no host to serve them on, | ||
| 113 | // and an instance that never wanted them pays no processes for the idea. | ||
| 114 | if cfg.PagesDomain != "" { | ||
| 115 | s.Pages = pages.New(pages.Config{ | ||
| 116 | Domain: cfg.PagesDomain, | ||
| 117 | Ref: cfg.PagesRef, | ||
| 118 | Procs: cfg.PagesProcs, | ||
| 119 | CacheBytes: cfg.PagesCacheBytes, | ||
| 120 | Log: log, | ||
| 121 | }, rs) | ||
| 122 | } | ||
| 110 | 123 | s.mux = s.routes() | |
| 111 | 124 | // Background loops start here rather than in Run: a caller that serves the | |
| 112 | 125 | // listeners itself (the tests, an embedder) must still get roster reloads, | |
| @@ -149,7 +162,20 @@ func (s *Server) routes() *http.ServeMux { | |||
| 149 | 162 | return mux | |
| 150 | 163 | } | |
| 151 | 164 | ||
| 152 | func (s *Server) Handler() http.Handler { return s.mux } | ||
| 165 | // Handler returns the HTTP surface. Site hosts are dispatched first: a request | ||
| 166 | // for <repo>.<pages_domain> is a page, and everything else is the forge UI. | ||
| 167 | func (s *Server) Handler() http.Handler { | ||
| 168 | if s.Pages == nil { | ||
| 169 | return s.mux | ||
| 170 | } | ||
| 171 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
| 172 | if _, ok := s.Pages.Host(r.Host); ok { | ||
| 173 | s.Pages.ServeHTTP(w, r) | ||
| 174 | return | ||
| 175 | } | ||
| 176 | s.mux.ServeHTTP(w, r) | ||
| 177 | }) | ||
| 178 | } | ||
| 153 | 179 | ||
| 154 | 180 | // Run starts both listeners and blocks until ctx is cancelled. | |
| 155 | 181 | func (s *Server) Run(ctx context.Context) error { | |
| @@ -221,6 +247,9 @@ func (s *Server) Close() error { | |||
| 221 | 247 | if s.cancel != nil { | |
| 222 | 248 | s.cancel() | |
| 223 | 249 | } | |
| 250 | if s.Pages != nil { | ||
| 251 | s.Pages.Close() | ||
| 252 | } | ||
| 224 | 253 | if s.Runs != nil { | |
| 225 | 254 | _ = s.Runs.Close() | |
| 226 | 255 | } | |