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
}
// 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)
}