package web
import (
"context"
"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 comes
// off the same tree walk as the archive, so a document listed here and a
// document in the archive are the same bytes at the same revision.
type Snapshot struct {
Ref core.SpaceRef
Rev string
Archive *doc.Archive
Bodies map[string][]byte
}
// Reader is the service 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. It is named Reader for the
// read plane it began as; Phase 4's review page adds the proposal reads and the
// two approve/reject writes, because the review page is where this package first
// mutates anything and one seam is simpler than two.
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)
// GetProposal resolves one proposal by id, in any state — the stable
// proposal URL still resolves after merge or rejection.
GetProposal(ctx context.Context, id int) (service.Proposal, error)
// ListProposals returns a space's proposals in one state, newest first: the
// inbox (open) and the digest (merged) are the same call with a different
// state.
ListProposals(ctx context.Context, ref core.SpaceRef, state core.ProposalState) ([]service.Proposal, error)
// ProposalDiff returns each document a proposal changes, with the base and
// proposed content the page diffs.
ProposalDiff(ctx context.Context, p service.Proposal) ([]service.ProposalDoc, error)
// Approve merges a proposal on the owner's approval — always human, never
// policy. Reject resolves it to rejected. Both return the proposal as it now
// stands.
Approve(ctx context.Context, ref core.SpaceRef, id int) (service.Proposal, error)
Reject(ctx context.Context, ref core.SpaceRef, id int) (service.Proposal, 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
}
// One call, one tree walk, and the link graph filled in. This package used
// to scan the git tree itself and then list the documents a second time for
// their bodies — two walks per page view, and a reach past service/ into
// gitx that the layering rule forbids.
arc, bodies, err := r.svc.Archive(ctx, sp, rev)
if err != nil {
return nil, err
}
return &Snapshot{Ref: ref, Rev: arc.Rev, 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)
}
func (r serviceReader) GetProposal(ctx context.Context, id int) (service.Proposal, error) {
return r.svc.GetProposal(ctx, id)
}
func (r serviceReader) ListProposals(ctx context.Context, ref core.SpaceRef, state core.ProposalState) ([]service.Proposal, error) {
return r.svc.ListProposals(ctx, ref, state)
}
func (r serviceReader) ProposalDiff(ctx context.Context, p service.Proposal) ([]service.ProposalDoc, error) {
return r.svc.ProposalDiff(ctx, p)
}
func (r serviceReader) Approve(ctx context.Context, ref core.SpaceRef, id int) (service.Proposal, error) {
return r.svc.MergeHuman(ctx, ref, id)
}
func (r serviceReader) Reject(ctx context.Context, ref core.SpaceRef, id int) (service.Proposal, error) {
return r.svc.Reject(ctx, ref, id)
}