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