Go · 4290 bytes Raw Blame History
1 // SPDX-License-Identifier: AGPL-3.0-or-later
2
3 package review
4
5 import (
6 "context"
7 "errors"
8 "fmt"
9 "strings"
10
11 "github.com/jackc/pgx/v5"
12 "github.com/jackc/pgx/v5/pgtype"
13
14 issuesdb "github.com/tenseleyFlow/shithub/internal/issues/sqlc"
15 pullsdb "github.com/tenseleyFlow/shithub/internal/pulls/sqlc"
16 )
17
18 // SubmitParams describes the submit-a-review action.
19 type SubmitParams struct {
20 PRIssueID int64
21 AuthorUserID int64
22 State string // "comment" | "approve" | "request_changes"
23 Body string
24 // PRAuthorUserID is the issues.author_user_id of the PR — used to
25 // reject self-approval. Caller passes it in to keep this package
26 // from cross-importing the pulls orchestrator.
27 PRAuthorUserID int64
28 }
29
30 // Submit records the review row, attaches every pending draft comment
31 // the author has on this PR to the new review (one tx), and emits a
32 // `reviewed` timeline event with the state. Pending review-request
33 // rows owned by the author are satisfied when state is approve or
34 // request_changes.
35 func Submit(ctx context.Context, deps Deps, p SubmitParams) (pullsdb.PrReview, error) {
36 if p.State != "comment" && p.State != "approve" && p.State != "request_changes" {
37 return pullsdb.PrReview{}, ErrInvalidState
38 }
39 if p.State == "approve" && p.AuthorUserID != 0 && p.AuthorUserID == p.PRAuthorUserID {
40 return pullsdb.PrReview{}, ErrAuthorCannotApprove
41 }
42 body := strings.TrimSpace(p.Body)
43 if len(body) > 65535 {
44 return pullsdb.PrReview{}, ErrBodyTooLong
45 }
46 html := renderBodyHTML(ctx, deps, body)
47
48 tx, err := deps.Pool.Begin(ctx)
49 if err != nil {
50 return pullsdb.PrReview{}, err
51 }
52 committed := false
53 defer func() {
54 if !committed {
55 _ = tx.Rollback(ctx)
56 }
57 }()
58
59 q := pullsdb.New()
60 row, err := q.CreatePRReview(ctx, tx, pullsdb.CreatePRReviewParams{
61 PrIssueID: p.PRIssueID,
62 AuthorUserID: pgtype.Int8{Int64: p.AuthorUserID, Valid: p.AuthorUserID != 0},
63 State: pullsdb.PrReviewState(p.State),
64 Body: body,
65 BodyHtmlCached: pgtype.Text{String: html, Valid: html != ""},
66 })
67 if err != nil {
68 return pullsdb.PrReview{}, fmt.Errorf("insert review: %w", err)
69 }
70
71 // Attach every pending draft comment authored by this user on this
72 // PR to the new review.
73 if err := q.AttachPendingCommentsToReview(ctx, tx, pullsdb.AttachPendingCommentsToReviewParams{
74 PrIssueID: p.PRIssueID,
75 AuthorUserID: pgtype.Int8{Int64: p.AuthorUserID, Valid: p.AuthorUserID != 0},
76 ReviewID: pgtype.Int8{Int64: row.ID, Valid: true},
77 }); err != nil {
78 return pullsdb.PrReview{}, fmt.Errorf("attach pending: %w", err)
79 }
80
81 // Satisfy any pending review request from the author (only on
82 // approve / request_changes per spec).
83 if p.State == "approve" || p.State == "request_changes" {
84 if err := q.SatisfyPRReviewRequest(ctx, tx, pullsdb.SatisfyPRReviewRequestParams{
85 PrIssueID: p.PRIssueID,
86 SatisfiedByReviewID: pgtype.Int8{Int64: row.ID, Valid: true},
87 RequestedUserID: pgtype.Int8{Int64: p.AuthorUserID, Valid: p.AuthorUserID != 0},
88 }); err != nil {
89 return pullsdb.PrReview{}, fmt.Errorf("satisfy request: %w", err)
90 }
91 }
92
93 // `reviewed` timeline event.
94 iq := issuesdb.New()
95 if _, err := iq.InsertIssueEvent(ctx, tx, issuesdb.InsertIssueEventParams{
96 IssueID: p.PRIssueID,
97 ActorUserID: pgtype.Int8{Int64: p.AuthorUserID, Valid: p.AuthorUserID != 0},
98 Kind: "reviewed",
99 Meta: []byte(fmt.Sprintf(`{"state":%q,"review_id":%d}`, p.State, row.ID)),
100 }); err != nil {
101 return pullsdb.PrReview{}, fmt.Errorf("emit event: %w", err)
102 }
103
104 if err := tx.Commit(ctx); err != nil {
105 return pullsdb.PrReview{}, err
106 }
107 committed = true
108 return row, nil
109 }
110
111 // Dismiss flips a review's dismissed_at + reason. Caller has already
112 // gated on policy (typically repo admin only).
113 func Dismiss(ctx context.Context, deps Deps, actorUserID, reviewID int64, reason string) error {
114 q := pullsdb.New()
115 if _, err := q.GetPRReviewByID(ctx, deps.Pool, reviewID); err != nil {
116 if errors.Is(err, pgx.ErrNoRows) {
117 return ErrReviewNotFound
118 }
119 return err
120 }
121 return q.DismissPRReview(ctx, deps.Pool, pullsdb.DismissPRReviewParams{
122 ID: reviewID,
123 DismissedByUserID: pgtype.Int8{Int64: actorUserID, Valid: actorUserID != 0},
124 DismissalReason: strings.TrimSpace(reason),
125 })
126 }
127