package web import ( "context" "fmt" "sourcecraft.dev/bigbes/sr-ht-spec/core" "sourcecraft.dev/bigbes/sr-ht-spec/doc" "sourcecraft.dev/bigbes/sr-ht-spec/search" "sourcecraft.dev/bigbes/sr-ht-spec/service" ) // Snapshot is one space at one *resolved* revision: the addressable document // set plus every document's bytes. // // Rev is always a commit sha, never a branch name, even when the request // carried no ?rev= at all. The read plane resolves first and renders second, so // a merge landing mid-request cannot make one page describe two revisions — // and the sha it resolved to is the value the page offers as its permalink. // // Bodies is keyed by tree path, the same key doc.Page.Path carries. It is // present because doc.Page.Links is filled by a render pass rather than by // Scan, so backlinks require rendering every document in the space; see // linkPass. type Snapshot struct { Ref core.SpaceRef Rev string Archive *doc.Archive Bodies map[string][]byte } // Reader is the read surface this package needs. *service.Service provides it // through NewReader. // // It is an interface for the same reason compare.sr.ht's authz.Authorizer is: // so the handlers can be tested against a document set rather than against a // Postgres instance and a tree of bare repositories. type Reader interface { // ListSpaces returns every space on the instance. Single-user with no // visibility levels means there is nothing to filter — the list is the // whole corpus. ListSpaces(ctx context.Context) ([]core.SpaceRef, error) // Snapshot resolves rev (service.ApprovedRev for the approved head) and // returns the space at that revision. Snapshot(ctx context.Context, ref core.SpaceRef, rev string) (*Snapshot, error) // ReadDocument reads one document by tree path at a revision. ReadDocument(ctx context.Context, ref core.SpaceRef, rev, path string) (service.Document, error) } // Searcher is the keyword index. *search.Index satisfies it as declared. type Searcher interface { Search(ctx context.Context, q search.Query) (search.Results, error) } // NewReader adapts a *service.Service to Reader. func NewReader(svc *service.Service) Reader { return serviceReader{svc: svc} } // serviceReader is the production Reader: everything goes through service/, // which is the layer that is allowed to touch gitx and db. type serviceReader struct{ svc *service.Service } func (r serviceReader) ListSpaces(ctx context.Context) ([]core.SpaceRef, error) { spaces, err := r.svc.ListSpaces(ctx) if err != nil { return nil, err } refs := make([]core.SpaceRef, 0, len(spaces)) for _, sp := range spaces { refs = append(refs, sp.Ref) } return refs, nil } func (r serviceReader) Snapshot(ctx context.Context, ref core.SpaceRef, rev string) (*Snapshot, error) { sp, err := r.svc.OpenSpace(ctx, ref) if err != nil { return nil, err } resolved, err := r.svc.ResolveRev(ctx, sp, rev) if err != nil { return nil, err } // Both reads below are issued against the resolved sha rather than against // the caller's rev, so the archive and the bodies are the same revision by // construction and cannot disagree if a merge lands between them. arc, err := doc.Scan(ctx, sp.Repo, ref, resolved) if err != nil { return nil, fmt.Errorf("web: scan %s at %s: %w", ref, resolved, err) } docs, err := r.svc.ListDocuments(ctx, sp, resolved) if err != nil { return nil, err } bodies := make(map[string][]byte, len(docs)) for _, d := range docs { bodies[d.Path] = d.Data } return &Snapshot{Ref: ref, Rev: resolved, Archive: arc, Bodies: bodies}, nil } func (r serviceReader) ReadDocument(ctx context.Context, ref core.SpaceRef, rev, p string) (service.Document, error) { sp, err := r.svc.OpenSpace(ctx, ref) if err != nil { return service.Document{}, err } return r.svc.ReadDocument(ctx, sp, rev, p) }