package core import ( "bytes" "encoding/json" "fmt" "html/template" "net/http" ) // TemplateSet holds one independently-parsed template tree per view. // Views are parsed separately because html/template has a single namespace // for defined blocks -- two views both defining "main" would collide. type TemplateSet map[string]*template.Template // Renderer turns a Ctx into bytes plus a content type. HTML pages and JSON // endpoints are the same shape of thing; this is where they unify. type Renderer interface { Render(c *Ctx, ts TemplateSet) ([]byte, error) ContentType() string } // View renders an HTML template. The template calls modules via `mod`. type View struct{ Template string } func (v View) ContentType() string { return "text/html; charset=utf-8" } func (v View) Render(c *Ctx, ts TemplateSet) ([]byte, error) { t, ok := ts[v.Template] if !ok { return nil, fmt.Errorf("no such view %q", v.Template) } // Clone per render so the funcmap can close over this Ctx. tt, err := t.Clone() if err != nil { return nil, err } tt = tt.Funcs(FuncMap(c)) var buf bytes.Buffer if err := tt.ExecuteTemplate(&buf, v.Template, c); err != nil { return nil, fmt.Errorf("view %s: %w", v.Template, err) } return buf.Bytes(), nil } // Endpoint builds the value that will be serialized. It must return a // purpose-built DTO, never a raw module value -- see internal/api. type Endpoint func(c *Ctx) (any, error) // JSON renders an API endpoint. type JSON struct{ Build Endpoint } func (j JSON) ContentType() string { return "application/json; charset=utf-8" } func (j JSON) Render(c *Ctx, _ TemplateSet) ([]byte, error) { v, err := j.Build(c) if err != nil { return nil, err } b, err := json.MarshalIndent(v, "", " ") if err != nil { return nil, err } return append(b, '\n'), nil } // ------------------------------------------------------------------ routes // Route binds a renderer to a net/http pattern (Go 1.22 wildcards). // // /{repo}/tree/{rest...} -> r.PathValue("repo"), r.PathValue("rest") // // Scope decides how the Ctx is built before Render runs. type Route struct { Name string Pattern string // e.g. "GET /{repo}/commit/{rev}" Render Renderer Scope Scope // Immutable marks responses whose content cannot change for a given // URL -- anything addressed by a commit SHA. Those get a year-long // Cache-Control; ref-addressed URLs get revalidation instead. Immutable bool // CacheableAnon opts a route into Server's in-memory response cache for // anonymous, shareless requests (see Server.cacheEligible) -- a full // rendered response is stored and replayed to the next matching // request within Ctx.cacheMaxAge, without touching git at all. Reserved // for the small-HTML page routes most likely to be a spike's hot page; // left off routes whose bodies can be large or unboundedly numerous // (blob, raw, compare -- see routes.go) and the JSON api-* endpoints, // to keep this pass's scope and memory footprint bounded: respCache is // an LRU bounded by entry count, not bytes. CacheableAnon bool // NavLabel, when set on a ScopeRepo route, adds a tab for it to the // repo-scoped navigation (see nav.go) -- the top tab bar and the // page-nav chips both derive from this instead of hardcoding links. NavLabel string // Request, when set, parses this route's path parameters. A nil parser // uses the default {rest...}/{rev} handling. Parsers only split path // text into ref candidates: core still calls gitx.Repo.Resolve, so user // input never reaches git as raw ref syntax (see AGENTS invariant 1). Request RequestFunc // PageSize, when greater than zero, enables ?ofs= pagination at that // page size and folds the normalized offset into Ctx.Vars["ofs"]. PageSize int } // Request is the path-derived input a route hands back to core before any // ref is resolved. Ref is the head commit; Range is set only for routes // that compare two commits. type Request struct { Ref string Path string Range *Range } // Range describes a base..head request. Ref on Request carries head. type Range struct { Base string ThreeDot bool } // RequestFunc parses route-specific path parameters. It must not invoke // git or resolve refs itself. type RequestFunc func(*http.Request) (Request, error) type Scope int const ( ScopeRepo Scope = iota // resolves {repo}, and {rest...}/{rev} if present ScopeSite // no repository in the URL ) // Page sizes for routes that paginate via ?ofs=. Kept in core (not // modules) so buildCtx can quantize an incoming ofs to a real page // boundary without an import cycle. Routes declare the matching PageSize, // and it must match the literal arg the log.html/graph.html template passes // to `mod "log"`/`mod "graph"`. const ( LogPageSize = 20 GraphPageSize = 150 ) // httpError lets a module or endpoint choose the status code. type httpError struct { Code int Err error } func (e httpError) Error() string { return e.Err.Error() } func (e httpError) Unwrap() error { return e.Err } func Errorf(code int, format string, a ...any) error { return httpError{Code: code, Err: fmt.Errorf(format, a...)} } func statusOf(err error) int { var he httpError if ok := asHTTPError(err, &he); ok { return he.Code } return http.StatusInternalServerError } // HeaderSetter lets a renderer add response headers. Optional: the server // type-asserts for it after Render succeeds. type HeaderSetter interface { Headers(c *Ctx, h http.Header) } // Raw serves file bytes straight out of the object database. // // SECURITY: a repository can contain an .html file with a