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 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 }