~bigbes/sr-ht-spec

ref: d8a5164f5f1c6ac49b056eb0cb1b9c3d9db338fd sr-ht-spec/service/review.go -rw-r--r-- 5.8 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
153
154
155
156
157
158
159
160
161
162
package service

import (
	"context"
	"fmt"
	"sort"

	"sourcecraft.dev/bigbes/sr-ht-spec/core"
)

// ProposalDoc is one document a proposal touches, as the review page needs it:
// its path, the approved content it was based on, and the proposed content on
// the branch. It is the input to the prose diff, which is the web layer's to
// render — this layer reads git and hands over bytes.
type ProposalDoc struct {
	// Path is the document's path on the proposal branch.
	Path string

	// Base is the document's content at the proposal's base — the approved text
	// the change was made against. Nil for a document the proposal adds, which
	// is the signal to render it as wholly new rather than as a diff.
	Base []byte

	// Proposed is the document's content on the proposal branch.
	Proposed []byte

	// New reports whether the document did not exist at the base.
	New bool
}

// ProposalDiff returns every document a proposal changes, each with the base and
// proposed content the review page diffs.
//
// It reads the base and the proposal branch through the normal pinned-revision
// path, not the ReadDocumentAtRef bypass: the branch tip is resolved to a commit
// sha first, and an object name is a legitimate read whatever it points at. The
// bypass exists for reading a branch *by name*; here the review already holds
// the proposal and can pin it.
//
// Only genuinely changed documents are returned — a proposal branch is cut from
// the base, so most of its documents are byte-identical to it and are not diffs.
// A proposal changes only documents (agents cannot rename or delete), so a
// document present at the base is present on the branch; the reverse asymmetry,
// a document added by the proposal, is marked New.
func (s *Service) ProposalDiff(ctx context.Context, p Proposal) ([]ProposalDoc, error) {
	sp, err := s.OpenSpace(ctx, p.Space)
	if err != nil {
		return nil, err
	}

	baseDocs, err := s.ListDocuments(ctx, sp, p.BaseRev)
	if err != nil {
		return nil, fmt.Errorf("service: read base %s of proposal %d: %w", short(p.BaseRev), p.ID, err)
	}
	base := make(map[string][]byte, len(baseDocs))
	for _, d := range baseDocs {
		base[d.Path] = d.Data
	}

	head, err := sp.Repo.BranchHead(ctx, p.Branch)
	if err != nil {
		return nil, readErr(err, "read head of %s in %s", p.Branch, p.Space)
	}
	branchDocs, err := s.ListDocuments(ctx, sp, head.String())
	if err != nil {
		return nil, fmt.Errorf("service: read proposal branch %s: %w", p.Branch, err)
	}

	var out []ProposalDoc
	for _, d := range branchDocs {
		prior, existed := base[d.Path]
		switch {
		case !existed:
			out = append(out, ProposalDoc{Path: d.Path, Proposed: d.Data, New: true})
		case !bytesEqual(prior, d.Data):
			out = append(out, ProposalDoc{Path: d.Path, Base: prior, Proposed: d.Data})
		}
	}
	sort.Slice(out, func(i, j int) bool { return out[i].Path < out[j].Path })
	return out, nil
}

// bytesEqual reports byte equality. It exists so ProposalDiff does not pull in
// bytes for a single comparison, and reads as intent at the call site.
func bytesEqual(a, b []byte) bool {
	if len(a) != len(b) {
		return false
	}
	for i := range a {
		if a[i] != b[i] {
			return false
		}
	}
	return true
}

// InboxProposals is every open proposal on the instance, newest first — the
// reviewer's queue, "N proposals waiting on you". It is instance-wide because
// there is one reviewer: a per-space inbox would make them visit each space to
// find what a link never reached them about.
//
// The digest and this share the mapping from a stored proposal's space_id back
// to a reference, done once from the space list rather than a lookup per row.
func (s *Service) InboxProposals(ctx context.Context) ([]Proposal, error) {
	return s.proposalsInState(ctx, core.StateOpen, false, 0)
}

// DigestProposals is the recently policy-merged proposals — the firehose
// digest. Auto-merged content never stops for review, so this is where a human
// sees it after the fact; the design's whole reason for keeping approval=policy
// distinct from human is so this list can exist. limit bounds it; <= 0 is a
// sane default.
func (s *Service) DigestProposals(ctx context.Context, limit int) ([]Proposal, error) {
	if limit <= 0 {
		limit = 20
	}
	return s.proposalsInState(ctx, core.StateMerged, true, limit)
}

// proposalsInState lists proposals in one state instance-wide, optionally
// keeping only the policy-approved ones (the digest), and maps each onto its
// space reference. A row whose space no longer lists is skipped rather than
// errored: a deleted space takes its proposals out of every human-facing view,
// and a dangling row is the reconciler's to notice, not this read's to fail on.
func (s *Service) proposalsInState(ctx context.Context, state core.ProposalState, policyOnly bool, limit int) ([]Proposal, error) {
	spaces, err := s.ListSpaces(ctx)
	if err != nil {
		return nil, err
	}
	refByID := make(map[int]core.SpaceRef, len(spaces))
	for _, sp := range spaces {
		refByID[sp.ID] = sp.Ref
	}

	rows, err := s.store.ListProposalsByState(ctx, state, 0)
	if err != nil {
		return nil, fmt.Errorf("service: list %s proposals: %w", state, err)
	}
	out := make([]Proposal, 0, len(rows))
	for _, p := range rows {
		if policyOnly && p.Approval != core.ApprovalPolicy {
			continue
		}
		ref, ok := refByID[p.SpaceID]
		if !ok {
			continue
		}
		out = append(out, proposalView(p, ref))
		if limit > 0 && len(out) == limit {
			break
		}
	}
	return out, nil
}

// MergeHuman lands a proposal on the owner's approval — the review page's
// approve button. It is Merge with the approval kind fixed, so the surface does
// not choose it: a browser approve is always human, and a caller that could pass
// ApprovalPolicy here would be able to launder a firehose merge as reviewed.
func (s *Service) MergeHuman(ctx context.Context, ref core.SpaceRef, proposalID int) (Proposal, error) {
	return s.Merge(ctx, ref, proposalID, core.ApprovalHuman)
}