~bigbes/sr-ht-spec

ref: d8a5164f5f1c6ac49b056eb0cb1b9c3d9db338fd sr-ht-spec/web/reader.go -rw-r--r-- 6.0 KiB
d8a5164f — Eugene Blikh feat(webhooks): fire on proposal open/merge/reject (Phase 5a) 25 days ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
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)

	// Inbox is every open proposal on the instance, newest first — the review
	// queue. Digest is the recently policy-merged proposals, the firehose a human
	// sees after the fact.
	Inbox(ctx context.Context) ([]service.Proposal, error)
	Digest(ctx context.Context) ([]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)
}

func (r serviceReader) Inbox(ctx context.Context) ([]service.Proposal, error) {
	return r.svc.InboxProposals(ctx)
}

func (r serviceReader) Digest(ctx context.Context) ([]service.Proposal, error) {
	return r.svc.DigestProposals(ctx, 0)
}