1// Copyright 2016 The go-github AUTHORS. All rights reserved.
2//
3// Use of this source code is governed by a BSD-style
4// license that can be found in the LICENSE file.
5
6package github
7
8import (
9	"context"
10	"errors"
11	"fmt"
12	"time"
13)
14
15var ErrMixedCommentStyles = errors.New("cannot use both position and side/line form comments")
16
17// PullRequestReview represents a review of a pull request.
18type PullRequestReview struct {
19	ID             *int64     `json:"id,omitempty"`
20	NodeID         *string    `json:"node_id,omitempty"`
21	User           *User      `json:"user,omitempty"`
22	Body           *string    `json:"body,omitempty"`
23	SubmittedAt    *time.Time `json:"submitted_at,omitempty"`
24	CommitID       *string    `json:"commit_id,omitempty"`
25	HTMLURL        *string    `json:"html_url,omitempty"`
26	PullRequestURL *string    `json:"pull_request_url,omitempty"`
27	State          *string    `json:"state,omitempty"`
28	// AuthorAssociation is the comment author's relationship to the issue's repository.
29	// Possible values are "COLLABORATOR", "CONTRIBUTOR", "FIRST_TIMER", "FIRST_TIME_CONTRIBUTOR", "MEMBER", "OWNER", or "NONE".
30	AuthorAssociation *string `json:"author_association,omitempty"`
31}
32
33func (p PullRequestReview) String() string {
34	return Stringify(p)
35}
36
37// DraftReviewComment represents a comment part of the review.
38type DraftReviewComment struct {
39	Path     *string `json:"path,omitempty"`
40	Position *int    `json:"position,omitempty"`
41	Body     *string `json:"body,omitempty"`
42
43	// The new comfort-fade-preview fields
44	StartSide *string `json:"start_side,omitempty"`
45	Side      *string `json:"side,omitempty"`
46	StartLine *int    `json:"start_line,omitempty"`
47	Line      *int    `json:"line,omitempty"`
48}
49
50func (c DraftReviewComment) String() string {
51	return Stringify(c)
52}
53
54// PullRequestReviewRequest represents a request to create a review.
55type PullRequestReviewRequest struct {
56	NodeID   *string               `json:"node_id,omitempty"`
57	CommitID *string               `json:"commit_id,omitempty"`
58	Body     *string               `json:"body,omitempty"`
59	Event    *string               `json:"event,omitempty"`
60	Comments []*DraftReviewComment `json:"comments,omitempty"`
61}
62
63func (r PullRequestReviewRequest) String() string {
64	return Stringify(r)
65}
66
67func (r *PullRequestReviewRequest) isComfortFadePreview() (bool, error) {
68	var isCF *bool
69	for _, comment := range r.Comments {
70		if comment == nil {
71			continue
72		}
73		hasPos := comment.Position != nil
74		hasComfortFade := (comment.StartSide != nil) || (comment.Side != nil) ||
75			(comment.StartLine != nil) || (comment.Line != nil)
76
77		switch {
78		case hasPos && hasComfortFade:
79			return false, ErrMixedCommentStyles
80		case hasPos && isCF != nil && *isCF:
81			return false, ErrMixedCommentStyles
82		case hasComfortFade && isCF != nil && !*isCF:
83			return false, ErrMixedCommentStyles
84		}
85		isCF = &hasComfortFade
86	}
87	if isCF != nil {
88		return *isCF, nil
89	}
90	return false, nil
91}
92
93// PullRequestReviewDismissalRequest represents a request to dismiss a review.
94type PullRequestReviewDismissalRequest struct {
95	Message *string `json:"message,omitempty"`
96}
97
98func (r PullRequestReviewDismissalRequest) String() string {
99	return Stringify(r)
100}
101
102// ListReviews lists all reviews on the specified pull request.
103//
104// GitHub API docs: https://docs.github.com/en/free-pro-team@latest/rest/reference/pulls/#list-reviews-for-a-pull-request
105func (s *PullRequestsService) ListReviews(ctx context.Context, owner, repo string, number int, opts *ListOptions) ([]*PullRequestReview, *Response, error) {
106	u := fmt.Sprintf("repos/%v/%v/pulls/%d/reviews", owner, repo, number)
107	u, err := addOptions(u, opts)
108	if err != nil {
109		return nil, nil, err
110	}
111
112	req, err := s.client.NewRequest("GET", u, nil)
113	if err != nil {
114		return nil, nil, err
115	}
116
117	var reviews []*PullRequestReview
118	resp, err := s.client.Do(ctx, req, &reviews)
119	if err != nil {
120		return nil, resp, err
121	}
122
123	return reviews, resp, nil
124}
125
126// GetReview fetches the specified pull request review.
127//
128// TODO: Follow up with GitHub support about an issue with this method's
129// returned error format and remove this comment once it's fixed.
130// Read more about it here - https://github.com/google/go-github/issues/540
131//
132// GitHub API docs: https://docs.github.com/en/free-pro-team@latest/rest/reference/pulls/#get-a-review-for-a-pull-request
133func (s *PullRequestsService) GetReview(ctx context.Context, owner, repo string, number int, reviewID int64) (*PullRequestReview, *Response, error) {
134	u := fmt.Sprintf("repos/%v/%v/pulls/%d/reviews/%d", owner, repo, number, reviewID)
135
136	req, err := s.client.NewRequest("GET", u, nil)
137	if err != nil {
138		return nil, nil, err
139	}
140
141	review := new(PullRequestReview)
142	resp, err := s.client.Do(ctx, req, review)
143	if err != nil {
144		return nil, resp, err
145	}
146
147	return review, resp, nil
148}
149
150// DeletePendingReview deletes the specified pull request pending review.
151//
152// TODO: Follow up with GitHub support about an issue with this method's
153// returned error format and remove this comment once it's fixed.
154// Read more about it here - https://github.com/google/go-github/issues/540
155//
156// GitHub API docs: https://docs.github.com/en/free-pro-team@latest/rest/reference/pulls/#delete-a-pending-review-for-a-pull-request
157func (s *PullRequestsService) DeletePendingReview(ctx context.Context, owner, repo string, number int, reviewID int64) (*PullRequestReview, *Response, error) {
158	u := fmt.Sprintf("repos/%v/%v/pulls/%d/reviews/%d", owner, repo, number, reviewID)
159
160	req, err := s.client.NewRequest("DELETE", u, nil)
161	if err != nil {
162		return nil, nil, err
163	}
164
165	review := new(PullRequestReview)
166	resp, err := s.client.Do(ctx, req, review)
167	if err != nil {
168		return nil, resp, err
169	}
170
171	return review, resp, nil
172}
173
174// ListReviewComments lists all the comments for the specified review.
175//
176// TODO: Follow up with GitHub support about an issue with this method's
177// returned error format and remove this comment once it's fixed.
178// Read more about it here - https://github.com/google/go-github/issues/540
179//
180// GitHub API docs: https://docs.github.com/en/free-pro-team@latest/rest/reference/pulls/#list-comments-for-a-pull-request-review
181func (s *PullRequestsService) ListReviewComments(ctx context.Context, owner, repo string, number int, reviewID int64, opts *ListOptions) ([]*PullRequestComment, *Response, error) {
182	u := fmt.Sprintf("repos/%v/%v/pulls/%d/reviews/%d/comments", owner, repo, number, reviewID)
183	u, err := addOptions(u, opts)
184	if err != nil {
185		return nil, nil, err
186	}
187
188	req, err := s.client.NewRequest("GET", u, nil)
189	if err != nil {
190		return nil, nil, err
191	}
192
193	var comments []*PullRequestComment
194	resp, err := s.client.Do(ctx, req, &comments)
195	if err != nil {
196		return nil, resp, err
197	}
198
199	return comments, resp, nil
200}
201
202// CreateReview creates a new review on the specified pull request.
203//
204// TODO: Follow up with GitHub support about an issue with this method's
205// returned error format and remove this comment once it's fixed.
206// Read more about it here - https://github.com/google/go-github/issues/540
207//
208// GitHub API docs: https://docs.github.com/en/free-pro-team@latest/rest/reference/pulls/#create-a-review-for-a-pull-request
209//
210// In order to use multi-line comments, you must use the "comfort fade" preview.
211// This replaces the use of the "Position" field in comments with 4 new fields:
212//   [Start]Side, and [Start]Line.
213// These new fields must be used for ALL comments (including single-line),
214// with the following restrictions (empirically observed, so subject to change).
215//
216// For single-line "comfort fade" comments, you must use:
217//
218//    Path:  &path,  // as before
219//    Body:  &body,  // as before
220//    Side:  &"RIGHT" (or "LEFT")
221//    Line:  &123,  // NOT THE SAME AS POSITION, this is an actual line number.
222//
223// If StartSide or StartLine is used with single-line comments, a 422 is returned.
224//
225// For multi-line "comfort fade" comments, you must use:
226//
227//    Path:      &path,  // as before
228//    Body:      &body,  // as before
229//    StartSide: &"RIGHT" (or "LEFT")
230//    Side:      &"RIGHT" (or "LEFT")
231//    StartLine: &120,
232//    Line:      &125,
233//
234// Suggested edits are made by commenting on the lines to replace, and including the
235// suggested edit in a block like this (it may be surrounded in non-suggestion markdown):
236//
237//    ```suggestion
238//    Use this instead.
239//    It is waaaaaay better.
240//    ```
241func (s *PullRequestsService) CreateReview(ctx context.Context, owner, repo string, number int, review *PullRequestReviewRequest) (*PullRequestReview, *Response, error) {
242	u := fmt.Sprintf("repos/%v/%v/pulls/%d/reviews", owner, repo, number)
243
244	req, err := s.client.NewRequest("POST", u, review)
245	if err != nil {
246		return nil, nil, err
247	}
248
249	// Detect which style of review comment is being used.
250	if isCF, err := review.isComfortFadePreview(); err != nil {
251		return nil, nil, err
252	} else if isCF {
253		// If the review comments are using the comfort fade preview fields,
254		// then pass the comfort fade header.
255		req.Header.Set("Accept", mediaTypeMultiLineCommentsPreview)
256	}
257
258	r := new(PullRequestReview)
259	resp, err := s.client.Do(ctx, req, r)
260	if err != nil {
261		return nil, resp, err
262	}
263
264	return r, resp, nil
265}
266
267// UpdateReview updates the review summary on the specified pull request.
268//
269// GitHub API docs: https://docs.github.com/en/free-pro-team@latest/rest/reference/pulls/#update-a-review-for-a-pull-request
270func (s *PullRequestsService) UpdateReview(ctx context.Context, owner, repo string, number int, reviewID int64, body string) (*PullRequestReview, *Response, error) {
271	opts := &struct {
272		Body string `json:"body"`
273	}{Body: body}
274	u := fmt.Sprintf("repos/%v/%v/pulls/%d/reviews/%d", owner, repo, number, reviewID)
275
276	req, err := s.client.NewRequest("PUT", u, opts)
277	if err != nil {
278		return nil, nil, err
279	}
280
281	review := &PullRequestReview{}
282	resp, err := s.client.Do(ctx, req, review)
283	if err != nil {
284		return nil, resp, err
285	}
286
287	return review, resp, nil
288}
289
290// SubmitReview submits a specified review on the specified pull request.
291//
292// TODO: Follow up with GitHub support about an issue with this method's
293// returned error format and remove this comment once it's fixed.
294// Read more about it here - https://github.com/google/go-github/issues/540
295//
296// GitHub API docs: https://docs.github.com/en/free-pro-team@latest/rest/reference/pulls/#submit-a-review-for-a-pull-request
297func (s *PullRequestsService) SubmitReview(ctx context.Context, owner, repo string, number int, reviewID int64, review *PullRequestReviewRequest) (*PullRequestReview, *Response, error) {
298	u := fmt.Sprintf("repos/%v/%v/pulls/%d/reviews/%d/events", owner, repo, number, reviewID)
299
300	req, err := s.client.NewRequest("POST", u, review)
301	if err != nil {
302		return nil, nil, err
303	}
304
305	r := new(PullRequestReview)
306	resp, err := s.client.Do(ctx, req, r)
307	if err != nil {
308		return nil, resp, err
309	}
310
311	return r, resp, nil
312}
313
314// DismissReview dismisses a specified review on the specified pull request.
315//
316// TODO: Follow up with GitHub support about an issue with this method's
317// returned error format and remove this comment once it's fixed.
318// Read more about it here - https://github.com/google/go-github/issues/540
319//
320// GitHub API docs: https://docs.github.com/en/free-pro-team@latest/rest/reference/pulls/#dismiss-a-review-for-a-pull-request
321func (s *PullRequestsService) DismissReview(ctx context.Context, owner, repo string, number int, reviewID int64, review *PullRequestReviewDismissalRequest) (*PullRequestReview, *Response, error) {
322	u := fmt.Sprintf("repos/%v/%v/pulls/%d/reviews/%d/dismissals", owner, repo, number, reviewID)
323
324	req, err := s.client.NewRequest("PUT", u, review)
325	if err != nil {
326		return nil, nil, err
327	}
328
329	r := new(PullRequestReview)
330	resp, err := s.client.Do(ctx, req, r)
331	if err != nil {
332		return nil, resp, err
333	}
334
335	return r, resp, nil
336}
337