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