API: Add pull review endpoints (#11224)
	
		
	
				
					
				
			* API: Added pull review read only endpoints * Update Structs, move Conversion, Refactor * refactor * lint & co * fix lint + refactor * add new Review state, rm unessesary, refacotr loadAttributes, convert patch to diff * add DeletePullReview * add paggination * draft1: Create & submit review * fix lint * fix lint * impruve test * DONT use GhostUser for loadReviewer * expose comments_count of a PullReview * infent GetCodeCommentsCount() * fixes * fix+impruve * some nits * Handle Ghosts 👻 * add TEST for GET apis * complete TESTS * add HTMLURL to PullReview responce * code format as per @lafriks * update swagger definition * Update routers/api/v1/repo/pull_review.go Co-authored-by: David Svantesson <davidsvantesson@gmail.com> * add comments Co-authored-by: Thomas Berger <loki@lokis-chaos.de> Co-authored-by: David Svantesson <davidsvantesson@gmail.com>tokarchuk/v1.17
							parent
							
								
									4ed7d2a2bb
								
							
						
					
					
						commit
						c97494a4f4
					
				@ -0,0 +1,120 @@ | 
				
			|||||||
 | 
					// Copyright 2020 The Gitea Authors. All rights reserved.
 | 
				
			||||||
 | 
					// Use of this source code is governed by a MIT-style
 | 
				
			||||||
 | 
					// license that can be found in the LICENSE file.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package integrations | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import ( | 
				
			||||||
 | 
						"fmt" | 
				
			||||||
 | 
						"net/http" | 
				
			||||||
 | 
						"testing" | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/models" | 
				
			||||||
 | 
						api "code.gitea.io/gitea/modules/structs" | 
				
			||||||
 | 
						"github.com/stretchr/testify/assert" | 
				
			||||||
 | 
					) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestAPIPullReview(t *testing.T) { | 
				
			||||||
 | 
						defer prepareTestEnv(t)() | 
				
			||||||
 | 
						pullIssue := models.AssertExistsAndLoadBean(t, &models.Issue{ID: 3}).(*models.Issue) | 
				
			||||||
 | 
						assert.NoError(t, pullIssue.LoadAttributes()) | 
				
			||||||
 | 
						repo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: pullIssue.RepoID}).(*models.Repository) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// test ListPullReviews
 | 
				
			||||||
 | 
						session := loginUser(t, "user2") | 
				
			||||||
 | 
						token := getTokenForLoggedInUser(t, session) | 
				
			||||||
 | 
						req := NewRequestf(t, http.MethodGet, "/api/v1/repos/%s/%s/pulls/%d/reviews?token=%s", repo.OwnerName, repo.Name, pullIssue.Index, token) | 
				
			||||||
 | 
						resp := session.MakeRequest(t, req, http.StatusOK) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var reviews []*api.PullReview | 
				
			||||||
 | 
						DecodeJSON(t, resp, &reviews) | 
				
			||||||
 | 
						if !assert.Len(t, reviews, 6) { | 
				
			||||||
 | 
							return | 
				
			||||||
 | 
						} | 
				
			||||||
 | 
						for _, r := range reviews { | 
				
			||||||
 | 
							assert.EqualValues(t, pullIssue.HTMLURL(), r.HTMLPullURL) | 
				
			||||||
 | 
						} | 
				
			||||||
 | 
						assert.EqualValues(t, 8, reviews[3].ID) | 
				
			||||||
 | 
						assert.EqualValues(t, "APPROVED", reviews[3].State) | 
				
			||||||
 | 
						assert.EqualValues(t, 0, reviews[3].CodeCommentsCount) | 
				
			||||||
 | 
						assert.EqualValues(t, true, reviews[3].Stale) | 
				
			||||||
 | 
						assert.EqualValues(t, false, reviews[3].Official) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						assert.EqualValues(t, 10, reviews[5].ID) | 
				
			||||||
 | 
						assert.EqualValues(t, "REQUEST_CHANGES", reviews[5].State) | 
				
			||||||
 | 
						assert.EqualValues(t, 1, reviews[5].CodeCommentsCount) | 
				
			||||||
 | 
						assert.EqualValues(t, 0, reviews[5].Reviewer.ID) // ghost user
 | 
				
			||||||
 | 
						assert.EqualValues(t, false, reviews[5].Stale) | 
				
			||||||
 | 
						assert.EqualValues(t, true, reviews[5].Official) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// test GetPullReview
 | 
				
			||||||
 | 
						req = NewRequestf(t, http.MethodGet, "/api/v1/repos/%s/%s/pulls/%d/reviews/%d?token=%s", repo.OwnerName, repo.Name, pullIssue.Index, reviews[3].ID, token) | 
				
			||||||
 | 
						resp = session.MakeRequest(t, req, http.StatusOK) | 
				
			||||||
 | 
						var review api.PullReview | 
				
			||||||
 | 
						DecodeJSON(t, resp, &review) | 
				
			||||||
 | 
						assert.EqualValues(t, *reviews[3], review) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/pulls/%d/reviews/%d?token=%s", repo.OwnerName, repo.Name, pullIssue.Index, reviews[5].ID, token) | 
				
			||||||
 | 
						resp = session.MakeRequest(t, req, http.StatusOK) | 
				
			||||||
 | 
						DecodeJSON(t, resp, &review) | 
				
			||||||
 | 
						assert.EqualValues(t, *reviews[5], review) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// test GetPullReviewComments
 | 
				
			||||||
 | 
						comment := models.AssertExistsAndLoadBean(t, &models.Comment{ID: 7}).(*models.Comment) | 
				
			||||||
 | 
						req = NewRequestf(t, http.MethodGet, "/api/v1/repos/%s/%s/pulls/%d/reviews/%d/comments?token=%s", repo.OwnerName, repo.Name, pullIssue.Index, 10, token) | 
				
			||||||
 | 
						resp = session.MakeRequest(t, req, http.StatusOK) | 
				
			||||||
 | 
						var reviewComments []*api.PullReviewComment | 
				
			||||||
 | 
						DecodeJSON(t, resp, &reviewComments) | 
				
			||||||
 | 
						assert.Len(t, reviewComments, 1) | 
				
			||||||
 | 
						assert.EqualValues(t, "Ghost", reviewComments[0].Reviewer.UserName) | 
				
			||||||
 | 
						assert.EqualValues(t, "a review from a deleted user", reviewComments[0].Body) | 
				
			||||||
 | 
						assert.EqualValues(t, comment.ID, reviewComments[0].ID) | 
				
			||||||
 | 
						assert.EqualValues(t, comment.UpdatedUnix, reviewComments[0].Updated.Unix()) | 
				
			||||||
 | 
						assert.EqualValues(t, comment.HTMLURL(), reviewComments[0].HTMLURL) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// test CreatePullReview
 | 
				
			||||||
 | 
						req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews?token=%s", repo.OwnerName, repo.Name, pullIssue.Index, token), &api.CreatePullReviewOptions{ | 
				
			||||||
 | 
							Body: "body1", | 
				
			||||||
 | 
							// Event: "" # will result in PENDING
 | 
				
			||||||
 | 
							Comments: []api.CreatePullReviewComment{{ | 
				
			||||||
 | 
								Path:       "README.md", | 
				
			||||||
 | 
								Body:       "first new line", | 
				
			||||||
 | 
								OldLineNum: 0, | 
				
			||||||
 | 
								NewLineNum: 1, | 
				
			||||||
 | 
							}, { | 
				
			||||||
 | 
								Path:       "README.md", | 
				
			||||||
 | 
								Body:       "first old line", | 
				
			||||||
 | 
								OldLineNum: 1, | 
				
			||||||
 | 
								NewLineNum: 0, | 
				
			||||||
 | 
							}, | 
				
			||||||
 | 
							}, | 
				
			||||||
 | 
						}) | 
				
			||||||
 | 
						resp = session.MakeRequest(t, req, http.StatusOK) | 
				
			||||||
 | 
						DecodeJSON(t, resp, &review) | 
				
			||||||
 | 
						assert.EqualValues(t, 6, review.ID) | 
				
			||||||
 | 
						assert.EqualValues(t, "PENDING", review.State) | 
				
			||||||
 | 
						assert.EqualValues(t, 2, review.CodeCommentsCount) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// test SubmitPullReview
 | 
				
			||||||
 | 
						req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews/%d?token=%s", repo.OwnerName, repo.Name, pullIssue.Index, review.ID, token), &api.SubmitPullReviewOptions{ | 
				
			||||||
 | 
							Event: "APPROVED", | 
				
			||||||
 | 
							Body:  "just two nits", | 
				
			||||||
 | 
						}) | 
				
			||||||
 | 
						resp = session.MakeRequest(t, req, http.StatusOK) | 
				
			||||||
 | 
						DecodeJSON(t, resp, &review) | 
				
			||||||
 | 
						assert.EqualValues(t, 6, review.ID) | 
				
			||||||
 | 
						assert.EqualValues(t, "APPROVED", review.State) | 
				
			||||||
 | 
						assert.EqualValues(t, 2, review.CodeCommentsCount) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// test DeletePullReview
 | 
				
			||||||
 | 
						req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews?token=%s", repo.OwnerName, repo.Name, pullIssue.Index, token), &api.CreatePullReviewOptions{ | 
				
			||||||
 | 
							Body:  "just a comment", | 
				
			||||||
 | 
							Event: "COMMENT", | 
				
			||||||
 | 
						}) | 
				
			||||||
 | 
						resp = session.MakeRequest(t, req, http.StatusOK) | 
				
			||||||
 | 
						DecodeJSON(t, resp, &review) | 
				
			||||||
 | 
						assert.EqualValues(t, "COMMENT", review.State) | 
				
			||||||
 | 
						assert.EqualValues(t, 0, review.CodeCommentsCount) | 
				
			||||||
 | 
						req = NewRequestf(t, http.MethodDelete, "/api/v1/repos/%s/%s/pulls/%d/reviews/%d?token=%s", repo.OwnerName, repo.Name, pullIssue.Index, review.ID, token) | 
				
			||||||
 | 
						resp = session.MakeRequest(t, req, http.StatusNoContent) | 
				
			||||||
 | 
					} | 
				
			||||||
@ -0,0 +1,127 @@ | 
				
			|||||||
 | 
					// Copyright 2020 The Gitea Authors. All rights reserved.
 | 
				
			||||||
 | 
					// Use of this source code is governed by a MIT-style
 | 
				
			||||||
 | 
					// license that can be found in the LICENSE file.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package convert | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import ( | 
				
			||||||
 | 
						"strings" | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/models" | 
				
			||||||
 | 
						api "code.gitea.io/gitea/modules/structs" | 
				
			||||||
 | 
					) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ToPullReview convert a review to api format
 | 
				
			||||||
 | 
					func ToPullReview(r *models.Review, doer *models.User) (*api.PullReview, error) { | 
				
			||||||
 | 
						if err := r.LoadAttributes(); err != nil { | 
				
			||||||
 | 
							if !models.IsErrUserNotExist(err) { | 
				
			||||||
 | 
								return nil, err | 
				
			||||||
 | 
							} | 
				
			||||||
 | 
							r.Reviewer = models.NewGhostUser() | 
				
			||||||
 | 
						} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						auth := false | 
				
			||||||
 | 
						if doer != nil { | 
				
			||||||
 | 
							auth = doer.IsAdmin || doer.ID == r.ReviewerID | 
				
			||||||
 | 
						} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						result := &api.PullReview{ | 
				
			||||||
 | 
							ID:                r.ID, | 
				
			||||||
 | 
							Reviewer:          ToUser(r.Reviewer, doer != nil, auth), | 
				
			||||||
 | 
							State:             api.ReviewStateUnknown, | 
				
			||||||
 | 
							Body:              r.Content, | 
				
			||||||
 | 
							CommitID:          r.CommitID, | 
				
			||||||
 | 
							Stale:             r.Stale, | 
				
			||||||
 | 
							Official:          r.Official, | 
				
			||||||
 | 
							CodeCommentsCount: r.GetCodeCommentsCount(), | 
				
			||||||
 | 
							Submitted:         r.CreatedUnix.AsTime(), | 
				
			||||||
 | 
							HTMLURL:           r.HTMLURL(), | 
				
			||||||
 | 
							HTMLPullURL:       r.Issue.HTMLURL(), | 
				
			||||||
 | 
						} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						switch r.Type { | 
				
			||||||
 | 
						case models.ReviewTypeApprove: | 
				
			||||||
 | 
							result.State = api.ReviewStateApproved | 
				
			||||||
 | 
						case models.ReviewTypeReject: | 
				
			||||||
 | 
							result.State = api.ReviewStateRequestChanges | 
				
			||||||
 | 
						case models.ReviewTypeComment: | 
				
			||||||
 | 
							result.State = api.ReviewStateComment | 
				
			||||||
 | 
						case models.ReviewTypePending: | 
				
			||||||
 | 
							result.State = api.ReviewStatePending | 
				
			||||||
 | 
						case models.ReviewTypeRequest: | 
				
			||||||
 | 
							result.State = api.ReviewStateRequestReview | 
				
			||||||
 | 
						} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return result, nil | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ToPullReviewList convert a list of review to it's api format
 | 
				
			||||||
 | 
					func ToPullReviewList(rl []*models.Review, doer *models.User) ([]*api.PullReview, error) { | 
				
			||||||
 | 
						result := make([]*api.PullReview, 0, len(rl)) | 
				
			||||||
 | 
						for i := range rl { | 
				
			||||||
 | 
							// show pending reviews only for the user who created them
 | 
				
			||||||
 | 
							if rl[i].Type == models.ReviewTypePending && !(doer.IsAdmin || doer.ID == rl[i].ReviewerID) { | 
				
			||||||
 | 
								continue | 
				
			||||||
 | 
							} | 
				
			||||||
 | 
							r, err := ToPullReview(rl[i], doer) | 
				
			||||||
 | 
							if err != nil { | 
				
			||||||
 | 
								return nil, err | 
				
			||||||
 | 
							} | 
				
			||||||
 | 
							result = append(result, r) | 
				
			||||||
 | 
						} | 
				
			||||||
 | 
						return result, nil | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ToPullReviewCommentList convert the CodeComments of an review to it's api format
 | 
				
			||||||
 | 
					func ToPullReviewCommentList(review *models.Review, doer *models.User) ([]*api.PullReviewComment, error) { | 
				
			||||||
 | 
						if err := review.LoadAttributes(); err != nil { | 
				
			||||||
 | 
							if !models.IsErrUserNotExist(err) { | 
				
			||||||
 | 
								return nil, err | 
				
			||||||
 | 
							} | 
				
			||||||
 | 
							review.Reviewer = models.NewGhostUser() | 
				
			||||||
 | 
						} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						apiComments := make([]*api.PullReviewComment, 0, len(review.CodeComments)) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						auth := false | 
				
			||||||
 | 
						if doer != nil { | 
				
			||||||
 | 
							auth = doer.IsAdmin || doer.ID == review.ReviewerID | 
				
			||||||
 | 
						} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, lines := range review.CodeComments { | 
				
			||||||
 | 
							for _, comments := range lines { | 
				
			||||||
 | 
								for _, comment := range comments { | 
				
			||||||
 | 
									apiComment := &api.PullReviewComment{ | 
				
			||||||
 | 
										ID:           comment.ID, | 
				
			||||||
 | 
										Body:         comment.Content, | 
				
			||||||
 | 
										Reviewer:     ToUser(review.Reviewer, doer != nil, auth), | 
				
			||||||
 | 
										ReviewID:     review.ID, | 
				
			||||||
 | 
										Created:      comment.CreatedUnix.AsTime(), | 
				
			||||||
 | 
										Updated:      comment.UpdatedUnix.AsTime(), | 
				
			||||||
 | 
										Path:         comment.TreePath, | 
				
			||||||
 | 
										CommitID:     comment.CommitSHA, | 
				
			||||||
 | 
										OrigCommitID: comment.OldRef, | 
				
			||||||
 | 
										DiffHunk:     patch2diff(comment.Patch), | 
				
			||||||
 | 
										HTMLURL:      comment.HTMLURL(), | 
				
			||||||
 | 
										HTMLPullURL:  review.Issue.APIURL(), | 
				
			||||||
 | 
									} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									if comment.Line < 0 { | 
				
			||||||
 | 
										apiComment.OldLineNum = comment.UnsignedLine() | 
				
			||||||
 | 
									} else { | 
				
			||||||
 | 
										apiComment.LineNum = comment.UnsignedLine() | 
				
			||||||
 | 
									} | 
				
			||||||
 | 
									apiComments = append(apiComments, apiComment) | 
				
			||||||
 | 
								} | 
				
			||||||
 | 
							} | 
				
			||||||
 | 
						} | 
				
			||||||
 | 
						return apiComments, nil | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func patch2diff(patch string) string { | 
				
			||||||
 | 
						split := strings.Split(patch, "\n@@") | 
				
			||||||
 | 
						if len(split) == 2 { | 
				
			||||||
 | 
							return "@@" + split[1] | 
				
			||||||
 | 
						} | 
				
			||||||
 | 
						return "" | 
				
			||||||
 | 
					} | 
				
			||||||
@ -0,0 +1,92 @@ | 
				
			|||||||
 | 
					// Copyright 2020 The Gitea Authors. All rights reserved.
 | 
				
			||||||
 | 
					// Use of this source code is governed by a MIT-style
 | 
				
			||||||
 | 
					// license that can be found in the LICENSE file.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package structs | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import ( | 
				
			||||||
 | 
						"time" | 
				
			||||||
 | 
					) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ReviewStateType review state type
 | 
				
			||||||
 | 
					type ReviewStateType string | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const ( | 
				
			||||||
 | 
						// ReviewStateApproved pr is approved
 | 
				
			||||||
 | 
						ReviewStateApproved ReviewStateType = "APPROVED" | 
				
			||||||
 | 
						// ReviewStatePending pr state is pending
 | 
				
			||||||
 | 
						ReviewStatePending ReviewStateType = "PENDING" | 
				
			||||||
 | 
						// ReviewStateComment is a comment review
 | 
				
			||||||
 | 
						ReviewStateComment ReviewStateType = "COMMENT" | 
				
			||||||
 | 
						// ReviewStateRequestChanges changes for pr are requested
 | 
				
			||||||
 | 
						ReviewStateRequestChanges ReviewStateType = "REQUEST_CHANGES" | 
				
			||||||
 | 
						// ReviewStateRequestReview review is requested from user
 | 
				
			||||||
 | 
						ReviewStateRequestReview ReviewStateType = "REQUEST_REVIEW" | 
				
			||||||
 | 
						// ReviewStateUnknown state of pr is unknown
 | 
				
			||||||
 | 
						ReviewStateUnknown ReviewStateType = "" | 
				
			||||||
 | 
					) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// PullReview represents a pull request review
 | 
				
			||||||
 | 
					type PullReview struct { | 
				
			||||||
 | 
						ID                int64           `json:"id"` | 
				
			||||||
 | 
						Reviewer          *User           `json:"user"` | 
				
			||||||
 | 
						State             ReviewStateType `json:"state"` | 
				
			||||||
 | 
						Body              string          `json:"body"` | 
				
			||||||
 | 
						CommitID          string          `json:"commit_id"` | 
				
			||||||
 | 
						Stale             bool            `json:"stale"` | 
				
			||||||
 | 
						Official          bool            `json:"official"` | 
				
			||||||
 | 
						CodeCommentsCount int             `json:"comments_count"` | 
				
			||||||
 | 
						// swagger:strfmt date-time
 | 
				
			||||||
 | 
						Submitted time.Time `json:"submitted_at"` | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						HTMLURL     string `json:"html_url"` | 
				
			||||||
 | 
						HTMLPullURL string `json:"pull_request_url"` | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// PullReviewComment represents a comment on a pull request review
 | 
				
			||||||
 | 
					type PullReviewComment struct { | 
				
			||||||
 | 
						ID       int64  `json:"id"` | 
				
			||||||
 | 
						Body     string `json:"body"` | 
				
			||||||
 | 
						Reviewer *User  `json:"user"` | 
				
			||||||
 | 
						ReviewID int64  `json:"pull_request_review_id"` | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// swagger:strfmt date-time
 | 
				
			||||||
 | 
						Created time.Time `json:"created_at"` | 
				
			||||||
 | 
						// swagger:strfmt date-time
 | 
				
			||||||
 | 
						Updated time.Time `json:"updated_at"` | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						Path         string `json:"path"` | 
				
			||||||
 | 
						CommitID     string `json:"commit_id"` | 
				
			||||||
 | 
						OrigCommitID string `json:"original_commit_id"` | 
				
			||||||
 | 
						DiffHunk     string `json:"diff_hunk"` | 
				
			||||||
 | 
						LineNum      uint64 `json:"position"` | 
				
			||||||
 | 
						OldLineNum   uint64 `json:"original_position"` | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						HTMLURL     string `json:"html_url"` | 
				
			||||||
 | 
						HTMLPullURL string `json:"pull_request_url"` | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// CreatePullReviewOptions are options to create a pull review
 | 
				
			||||||
 | 
					type CreatePullReviewOptions struct { | 
				
			||||||
 | 
						Event    ReviewStateType           `json:"event"` | 
				
			||||||
 | 
						Body     string                    `json:"body"` | 
				
			||||||
 | 
						CommitID string                    `json:"commit_id"` | 
				
			||||||
 | 
						Comments []CreatePullReviewComment `json:"comments"` | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// CreatePullReviewComment represent a review comment for creation api
 | 
				
			||||||
 | 
					type CreatePullReviewComment struct { | 
				
			||||||
 | 
						// the tree path
 | 
				
			||||||
 | 
						Path string `json:"path"` | 
				
			||||||
 | 
						Body string `json:"body"` | 
				
			||||||
 | 
						// if comment to old file line or 0
 | 
				
			||||||
 | 
						OldLineNum int64 `json:"old_position"` | 
				
			||||||
 | 
						// if comment to new file line or 0
 | 
				
			||||||
 | 
						NewLineNum int64 `json:"new_position"` | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// SubmitPullReviewOptions are options to submit a pending pull review
 | 
				
			||||||
 | 
					type SubmitPullReviewOptions struct { | 
				
			||||||
 | 
						Event ReviewStateType `json:"event"` | 
				
			||||||
 | 
						Body  string          `json:"body"` | 
				
			||||||
 | 
					} | 
				
			||||||
@ -0,0 +1,522 @@ | 
				
			|||||||
 | 
					// Copyright 2020 The Gitea Authors. All rights reserved.
 | 
				
			||||||
 | 
					// Use of this source code is governed by a MIT-style
 | 
				
			||||||
 | 
					// license that can be found in the LICENSE file.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package repo | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import ( | 
				
			||||||
 | 
						"fmt" | 
				
			||||||
 | 
						"net/http" | 
				
			||||||
 | 
						"strings" | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/models" | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/context" | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/convert" | 
				
			||||||
 | 
						api "code.gitea.io/gitea/modules/structs" | 
				
			||||||
 | 
						"code.gitea.io/gitea/routers/api/v1/utils" | 
				
			||||||
 | 
						pull_service "code.gitea.io/gitea/services/pull" | 
				
			||||||
 | 
					) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ListPullReviews lists all reviews of a pull request
 | 
				
			||||||
 | 
					func ListPullReviews(ctx *context.APIContext) { | 
				
			||||||
 | 
						// swagger:operation GET /repos/{owner}/{repo}/pulls/{index}/reviews repository repoListPullReviews
 | 
				
			||||||
 | 
						// ---
 | 
				
			||||||
 | 
						// summary: List all reviews for a pull request
 | 
				
			||||||
 | 
						// produces:
 | 
				
			||||||
 | 
						// - application/json
 | 
				
			||||||
 | 
						// parameters:
 | 
				
			||||||
 | 
						// - name: owner
 | 
				
			||||||
 | 
						//   in: path
 | 
				
			||||||
 | 
						//   description: owner of the repo
 | 
				
			||||||
 | 
						//   type: string
 | 
				
			||||||
 | 
						//   required: true
 | 
				
			||||||
 | 
						// - name: repo
 | 
				
			||||||
 | 
						//   in: path
 | 
				
			||||||
 | 
						//   description: name of the repo
 | 
				
			||||||
 | 
						//   type: string
 | 
				
			||||||
 | 
						//   required: true
 | 
				
			||||||
 | 
						// - name: index
 | 
				
			||||||
 | 
						//   in: path
 | 
				
			||||||
 | 
						//   description: index of the pull request
 | 
				
			||||||
 | 
						//   type: integer
 | 
				
			||||||
 | 
						//   format: int64
 | 
				
			||||||
 | 
						//   required: true
 | 
				
			||||||
 | 
						// - name: page
 | 
				
			||||||
 | 
						//   in: query
 | 
				
			||||||
 | 
						//   description: page number of results to return (1-based)
 | 
				
			||||||
 | 
						//   type: integer
 | 
				
			||||||
 | 
						// - name: limit
 | 
				
			||||||
 | 
						//   in: query
 | 
				
			||||||
 | 
						//   description: page size of results, maximum page size is 50
 | 
				
			||||||
 | 
						//   type: integer
 | 
				
			||||||
 | 
						// responses:
 | 
				
			||||||
 | 
						//   "200":
 | 
				
			||||||
 | 
						//     "$ref": "#/responses/PullReviewList"
 | 
				
			||||||
 | 
						//   "404":
 | 
				
			||||||
 | 
						//     "$ref": "#/responses/notFound"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						pr, err := models.GetPullRequestByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) | 
				
			||||||
 | 
						if err != nil { | 
				
			||||||
 | 
							if models.IsErrPullRequestNotExist(err) { | 
				
			||||||
 | 
								ctx.NotFound("GetPullRequestByIndex", err) | 
				
			||||||
 | 
							} else { | 
				
			||||||
 | 
								ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err) | 
				
			||||||
 | 
							} | 
				
			||||||
 | 
							return | 
				
			||||||
 | 
						} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err = pr.LoadIssue(); err != nil { | 
				
			||||||
 | 
							ctx.Error(http.StatusInternalServerError, "LoadIssue", err) | 
				
			||||||
 | 
							return | 
				
			||||||
 | 
						} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err = pr.Issue.LoadRepo(); err != nil { | 
				
			||||||
 | 
							ctx.Error(http.StatusInternalServerError, "LoadRepo", err) | 
				
			||||||
 | 
							return | 
				
			||||||
 | 
						} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						allReviews, err := models.FindReviews(models.FindReviewOptions{ | 
				
			||||||
 | 
							ListOptions: utils.GetListOptions(ctx), | 
				
			||||||
 | 
							Type:        models.ReviewTypeUnknown, | 
				
			||||||
 | 
							IssueID:     pr.IssueID, | 
				
			||||||
 | 
						}) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err != nil { | 
				
			||||||
 | 
							ctx.Error(http.StatusInternalServerError, "FindReviews", err) | 
				
			||||||
 | 
							return | 
				
			||||||
 | 
						} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						apiReviews, err := convert.ToPullReviewList(allReviews, ctx.User) | 
				
			||||||
 | 
						if err != nil { | 
				
			||||||
 | 
							ctx.Error(http.StatusInternalServerError, "convertToPullReviewList", err) | 
				
			||||||
 | 
							return | 
				
			||||||
 | 
						} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						ctx.JSON(http.StatusOK, &apiReviews) | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetPullReview gets a specific review of a pull request
 | 
				
			||||||
 | 
					func GetPullReview(ctx *context.APIContext) { | 
				
			||||||
 | 
						// swagger:operation GET /repos/{owner}/{repo}/pulls/{index}/reviews/{id} repository repoGetPullReview
 | 
				
			||||||
 | 
						// ---
 | 
				
			||||||
 | 
						// summary: Get a specific review for a pull request
 | 
				
			||||||
 | 
						// produces:
 | 
				
			||||||
 | 
						// - application/json
 | 
				
			||||||
 | 
						// parameters:
 | 
				
			||||||
 | 
						// - name: owner
 | 
				
			||||||
 | 
						//   in: path
 | 
				
			||||||
 | 
						//   description: owner of the repo
 | 
				
			||||||
 | 
						//   type: string
 | 
				
			||||||
 | 
						//   required: true
 | 
				
			||||||
 | 
						// - name: repo
 | 
				
			||||||
 | 
						//   in: path
 | 
				
			||||||
 | 
						//   description: name of the repo
 | 
				
			||||||
 | 
						//   type: string
 | 
				
			||||||
 | 
						//   required: true
 | 
				
			||||||
 | 
						// - name: index
 | 
				
			||||||
 | 
						//   in: path
 | 
				
			||||||
 | 
						//   description: index of the pull request
 | 
				
			||||||
 | 
						//   type: integer
 | 
				
			||||||
 | 
						//   format: int64
 | 
				
			||||||
 | 
						//   required: true
 | 
				
			||||||
 | 
						// - name: id
 | 
				
			||||||
 | 
						//   in: path
 | 
				
			||||||
 | 
						//   description: id of the review
 | 
				
			||||||
 | 
						//   type: integer
 | 
				
			||||||
 | 
						//   format: int64
 | 
				
			||||||
 | 
						//   required: true
 | 
				
			||||||
 | 
						// responses:
 | 
				
			||||||
 | 
						//   "200":
 | 
				
			||||||
 | 
						//     "$ref": "#/responses/PullReview"
 | 
				
			||||||
 | 
						//   "404":
 | 
				
			||||||
 | 
						//     "$ref": "#/responses/notFound"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						review, _, statusSet := prepareSingleReview(ctx) | 
				
			||||||
 | 
						if statusSet { | 
				
			||||||
 | 
							return | 
				
			||||||
 | 
						} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						apiReview, err := convert.ToPullReview(review, ctx.User) | 
				
			||||||
 | 
						if err != nil { | 
				
			||||||
 | 
							ctx.Error(http.StatusInternalServerError, "convertToPullReview", err) | 
				
			||||||
 | 
							return | 
				
			||||||
 | 
						} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						ctx.JSON(http.StatusOK, apiReview) | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetPullReviewComments lists all comments of a pull request review
 | 
				
			||||||
 | 
					func GetPullReviewComments(ctx *context.APIContext) { | 
				
			||||||
 | 
						// swagger:operation GET /repos/{owner}/{repo}/pulls/{index}/reviews/{id}/comments repository repoGetPullReviewComments
 | 
				
			||||||
 | 
						// ---
 | 
				
			||||||
 | 
						// summary: Get a specific review for a pull request
 | 
				
			||||||
 | 
						// produces:
 | 
				
			||||||
 | 
						// - application/json
 | 
				
			||||||
 | 
						// parameters:
 | 
				
			||||||
 | 
						// - name: owner
 | 
				
			||||||
 | 
						//   in: path
 | 
				
			||||||
 | 
						//   description: owner of the repo
 | 
				
			||||||
 | 
						//   type: string
 | 
				
			||||||
 | 
						//   required: true
 | 
				
			||||||
 | 
						// - name: repo
 | 
				
			||||||
 | 
						//   in: path
 | 
				
			||||||
 | 
						//   description: name of the repo
 | 
				
			||||||
 | 
						//   type: string
 | 
				
			||||||
 | 
						//   required: true
 | 
				
			||||||
 | 
						// - name: index
 | 
				
			||||||
 | 
						//   in: path
 | 
				
			||||||
 | 
						//   description: index of the pull request
 | 
				
			||||||
 | 
						//   type: integer
 | 
				
			||||||
 | 
						//   format: int64
 | 
				
			||||||
 | 
						//   required: true
 | 
				
			||||||
 | 
						// - name: id
 | 
				
			||||||
 | 
						//   in: path
 | 
				
			||||||
 | 
						//   description: id of the review
 | 
				
			||||||
 | 
						//   type: integer
 | 
				
			||||||
 | 
						//   format: int64
 | 
				
			||||||
 | 
						//   required: true
 | 
				
			||||||
 | 
						// responses:
 | 
				
			||||||
 | 
						//   "200":
 | 
				
			||||||
 | 
						//     "$ref": "#/responses/PullReviewCommentList"
 | 
				
			||||||
 | 
						//   "404":
 | 
				
			||||||
 | 
						//     "$ref": "#/responses/notFound"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						review, _, statusSet := prepareSingleReview(ctx) | 
				
			||||||
 | 
						if statusSet { | 
				
			||||||
 | 
							return | 
				
			||||||
 | 
						} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						apiComments, err := convert.ToPullReviewCommentList(review, ctx.User) | 
				
			||||||
 | 
						if err != nil { | 
				
			||||||
 | 
							ctx.Error(http.StatusInternalServerError, "convertToPullReviewCommentList", err) | 
				
			||||||
 | 
							return | 
				
			||||||
 | 
						} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						ctx.JSON(http.StatusOK, apiComments) | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// DeletePullReview delete a specific review from a pull request
 | 
				
			||||||
 | 
					func DeletePullReview(ctx *context.APIContext) { | 
				
			||||||
 | 
						// swagger:operation DELETE /repos/{owner}/{repo}/pulls/{index}/reviews/{id} repository repoDeletePullReview
 | 
				
			||||||
 | 
						// ---
 | 
				
			||||||
 | 
						// summary: Delete a specific review from a pull request
 | 
				
			||||||
 | 
						// produces:
 | 
				
			||||||
 | 
						// - application/json
 | 
				
			||||||
 | 
						// parameters:
 | 
				
			||||||
 | 
						// - name: owner
 | 
				
			||||||
 | 
						//   in: path
 | 
				
			||||||
 | 
						//   description: owner of the repo
 | 
				
			||||||
 | 
						//   type: string
 | 
				
			||||||
 | 
						//   required: true
 | 
				
			||||||
 | 
						// - name: repo
 | 
				
			||||||
 | 
						//   in: path
 | 
				
			||||||
 | 
						//   description: name of the repo
 | 
				
			||||||
 | 
						//   type: string
 | 
				
			||||||
 | 
						//   required: true
 | 
				
			||||||
 | 
						// - name: index
 | 
				
			||||||
 | 
						//   in: path
 | 
				
			||||||
 | 
						//   description: index of the pull request
 | 
				
			||||||
 | 
						//   type: integer
 | 
				
			||||||
 | 
						//   format: int64
 | 
				
			||||||
 | 
						//   required: true
 | 
				
			||||||
 | 
						// - name: id
 | 
				
			||||||
 | 
						//   in: path
 | 
				
			||||||
 | 
						//   description: id of the review
 | 
				
			||||||
 | 
						//   type: integer
 | 
				
			||||||
 | 
						//   format: int64
 | 
				
			||||||
 | 
						//   required: true
 | 
				
			||||||
 | 
						// responses:
 | 
				
			||||||
 | 
						//   "204":
 | 
				
			||||||
 | 
						//     "$ref": "#/responses/empty"
 | 
				
			||||||
 | 
						//   "403":
 | 
				
			||||||
 | 
						//     "$ref": "#/responses/forbidden"
 | 
				
			||||||
 | 
						//   "404":
 | 
				
			||||||
 | 
						//     "$ref": "#/responses/notFound"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						review, _, statusSet := prepareSingleReview(ctx) | 
				
			||||||
 | 
						if statusSet { | 
				
			||||||
 | 
							return | 
				
			||||||
 | 
						} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if ctx.User == nil { | 
				
			||||||
 | 
							ctx.NotFound() | 
				
			||||||
 | 
							return | 
				
			||||||
 | 
						} | 
				
			||||||
 | 
						if !ctx.User.IsAdmin && ctx.User.ID != review.ReviewerID { | 
				
			||||||
 | 
							ctx.Error(http.StatusForbidden, "only admin and user itself can delete a review", nil) | 
				
			||||||
 | 
							return | 
				
			||||||
 | 
						} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := models.DeleteReview(review); err != nil { | 
				
			||||||
 | 
							ctx.Error(http.StatusInternalServerError, "DeleteReview", fmt.Errorf("can not delete ReviewID: %d", review.ID)) | 
				
			||||||
 | 
							return | 
				
			||||||
 | 
						} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						ctx.Status(http.StatusNoContent) | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// CreatePullReview create a review to an pull request
 | 
				
			||||||
 | 
					func CreatePullReview(ctx *context.APIContext, opts api.CreatePullReviewOptions) { | 
				
			||||||
 | 
						// swagger:operation POST /repos/{owner}/{repo}/pulls/{index}/reviews repository repoCreatePullReview
 | 
				
			||||||
 | 
						// ---
 | 
				
			||||||
 | 
						// summary: Create a review to an pull request
 | 
				
			||||||
 | 
						// produces:
 | 
				
			||||||
 | 
						// - application/json
 | 
				
			||||||
 | 
						// parameters:
 | 
				
			||||||
 | 
						// - name: owner
 | 
				
			||||||
 | 
						//   in: path
 | 
				
			||||||
 | 
						//   description: owner of the repo
 | 
				
			||||||
 | 
						//   type: string
 | 
				
			||||||
 | 
						//   required: true
 | 
				
			||||||
 | 
						// - name: repo
 | 
				
			||||||
 | 
						//   in: path
 | 
				
			||||||
 | 
						//   description: name of the repo
 | 
				
			||||||
 | 
						//   type: string
 | 
				
			||||||
 | 
						//   required: true
 | 
				
			||||||
 | 
						// - name: index
 | 
				
			||||||
 | 
						//   in: path
 | 
				
			||||||
 | 
						//   description: index of the pull request
 | 
				
			||||||
 | 
						//   type: integer
 | 
				
			||||||
 | 
						//   format: int64
 | 
				
			||||||
 | 
						//   required: true
 | 
				
			||||||
 | 
						// - name: body
 | 
				
			||||||
 | 
						//   in: body
 | 
				
			||||||
 | 
						//   required: true
 | 
				
			||||||
 | 
						//   schema:
 | 
				
			||||||
 | 
						//     "$ref": "#/definitions/CreatePullReviewOptions"
 | 
				
			||||||
 | 
						// responses:
 | 
				
			||||||
 | 
						//   "200":
 | 
				
			||||||
 | 
						//     "$ref": "#/responses/PullReview"
 | 
				
			||||||
 | 
						//   "404":
 | 
				
			||||||
 | 
						//     "$ref": "#/responses/notFound"
 | 
				
			||||||
 | 
						//   "422":
 | 
				
			||||||
 | 
						//     "$ref": "#/responses/validationError"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						pr, err := models.GetPullRequestByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) | 
				
			||||||
 | 
						if err != nil { | 
				
			||||||
 | 
							if models.IsErrPullRequestNotExist(err) { | 
				
			||||||
 | 
								ctx.NotFound("GetPullRequestByIndex", err) | 
				
			||||||
 | 
							} else { | 
				
			||||||
 | 
								ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err) | 
				
			||||||
 | 
							} | 
				
			||||||
 | 
							return | 
				
			||||||
 | 
						} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// determine review type
 | 
				
			||||||
 | 
						reviewType, isWrong := preparePullReviewType(ctx, pr, opts.Event, opts.Body) | 
				
			||||||
 | 
						if isWrong { | 
				
			||||||
 | 
							return | 
				
			||||||
 | 
						} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := pr.Issue.LoadRepo(); err != nil { | 
				
			||||||
 | 
							ctx.Error(http.StatusInternalServerError, "pr.Issue.LoadRepo", err) | 
				
			||||||
 | 
							return | 
				
			||||||
 | 
						} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// create review comments
 | 
				
			||||||
 | 
						for _, c := range opts.Comments { | 
				
			||||||
 | 
							line := c.NewLineNum | 
				
			||||||
 | 
							if c.OldLineNum > 0 { | 
				
			||||||
 | 
								line = c.OldLineNum * -1 | 
				
			||||||
 | 
							} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if _, err := pull_service.CreateCodeComment( | 
				
			||||||
 | 
								ctx.User, | 
				
			||||||
 | 
								ctx.Repo.GitRepo, | 
				
			||||||
 | 
								pr.Issue, | 
				
			||||||
 | 
								line, | 
				
			||||||
 | 
								c.Body, | 
				
			||||||
 | 
								c.Path, | 
				
			||||||
 | 
								true, // is review
 | 
				
			||||||
 | 
								0,    // no reply
 | 
				
			||||||
 | 
								opts.CommitID, | 
				
			||||||
 | 
							); err != nil { | 
				
			||||||
 | 
								ctx.ServerError("CreateCodeComment", err) | 
				
			||||||
 | 
								return | 
				
			||||||
 | 
							} | 
				
			||||||
 | 
						} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// create review and associate all pending review comments
 | 
				
			||||||
 | 
						review, _, err := pull_service.SubmitReview(ctx.User, ctx.Repo.GitRepo, pr.Issue, reviewType, opts.Body, opts.CommitID) | 
				
			||||||
 | 
						if err != nil { | 
				
			||||||
 | 
							ctx.Error(http.StatusInternalServerError, "SubmitReview", err) | 
				
			||||||
 | 
							return | 
				
			||||||
 | 
						} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// convert response
 | 
				
			||||||
 | 
						apiReview, err := convert.ToPullReview(review, ctx.User) | 
				
			||||||
 | 
						if err != nil { | 
				
			||||||
 | 
							ctx.Error(http.StatusInternalServerError, "convertToPullReview", err) | 
				
			||||||
 | 
							return | 
				
			||||||
 | 
						} | 
				
			||||||
 | 
						ctx.JSON(http.StatusOK, apiReview) | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// SubmitPullReview submit a pending review to an pull request
 | 
				
			||||||
 | 
					func SubmitPullReview(ctx *context.APIContext, opts api.SubmitPullReviewOptions) { | 
				
			||||||
 | 
						// swagger:operation POST /repos/{owner}/{repo}/pulls/{index}/reviews/{id} repository repoSubmitPullReview
 | 
				
			||||||
 | 
						// ---
 | 
				
			||||||
 | 
						// summary: Submit a pending review to an pull request
 | 
				
			||||||
 | 
						// produces:
 | 
				
			||||||
 | 
						// - application/json
 | 
				
			||||||
 | 
						// parameters:
 | 
				
			||||||
 | 
						// - name: owner
 | 
				
			||||||
 | 
						//   in: path
 | 
				
			||||||
 | 
						//   description: owner of the repo
 | 
				
			||||||
 | 
						//   type: string
 | 
				
			||||||
 | 
						//   required: true
 | 
				
			||||||
 | 
						// - name: repo
 | 
				
			||||||
 | 
						//   in: path
 | 
				
			||||||
 | 
						//   description: name of the repo
 | 
				
			||||||
 | 
						//   type: string
 | 
				
			||||||
 | 
						//   required: true
 | 
				
			||||||
 | 
						// - name: index
 | 
				
			||||||
 | 
						//   in: path
 | 
				
			||||||
 | 
						//   description: index of the pull request
 | 
				
			||||||
 | 
						//   type: integer
 | 
				
			||||||
 | 
						//   format: int64
 | 
				
			||||||
 | 
						//   required: true
 | 
				
			||||||
 | 
						// - name: id
 | 
				
			||||||
 | 
						//   in: path
 | 
				
			||||||
 | 
						//   description: id of the review
 | 
				
			||||||
 | 
						//   type: integer
 | 
				
			||||||
 | 
						//   format: int64
 | 
				
			||||||
 | 
						//   required: true
 | 
				
			||||||
 | 
						// - name: body
 | 
				
			||||||
 | 
						//   in: body
 | 
				
			||||||
 | 
						//   required: true
 | 
				
			||||||
 | 
						//   schema:
 | 
				
			||||||
 | 
						//     "$ref": "#/definitions/SubmitPullReviewOptions"
 | 
				
			||||||
 | 
						// responses:
 | 
				
			||||||
 | 
						//   "200":
 | 
				
			||||||
 | 
						//     "$ref": "#/responses/PullReview"
 | 
				
			||||||
 | 
						//   "404":
 | 
				
			||||||
 | 
						//     "$ref": "#/responses/notFound"
 | 
				
			||||||
 | 
						//   "422":
 | 
				
			||||||
 | 
						//     "$ref": "#/responses/validationError"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						review, pr, isWrong := prepareSingleReview(ctx) | 
				
			||||||
 | 
						if isWrong { | 
				
			||||||
 | 
							return | 
				
			||||||
 | 
						} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if review.Type != models.ReviewTypePending { | 
				
			||||||
 | 
							ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("only a pending review can be submitted")) | 
				
			||||||
 | 
							return | 
				
			||||||
 | 
						} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// determine review type
 | 
				
			||||||
 | 
						reviewType, isWrong := preparePullReviewType(ctx, pr, opts.Event, opts.Body) | 
				
			||||||
 | 
						if isWrong { | 
				
			||||||
 | 
							return | 
				
			||||||
 | 
						} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// if review stay pending return
 | 
				
			||||||
 | 
						if reviewType == models.ReviewTypePending { | 
				
			||||||
 | 
							ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("review stay pending")) | 
				
			||||||
 | 
							return | 
				
			||||||
 | 
						} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						headCommitID, err := ctx.Repo.GitRepo.GetRefCommitID(pr.GetGitRefName()) | 
				
			||||||
 | 
						if err != nil { | 
				
			||||||
 | 
							ctx.Error(http.StatusInternalServerError, "GitRepo: GetRefCommitID", err) | 
				
			||||||
 | 
							return | 
				
			||||||
 | 
						} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// create review and associate all pending review comments
 | 
				
			||||||
 | 
						review, _, err = pull_service.SubmitReview(ctx.User, ctx.Repo.GitRepo, pr.Issue, reviewType, opts.Body, headCommitID) | 
				
			||||||
 | 
						if err != nil { | 
				
			||||||
 | 
							ctx.Error(http.StatusInternalServerError, "SubmitReview", err) | 
				
			||||||
 | 
							return | 
				
			||||||
 | 
						} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// convert response
 | 
				
			||||||
 | 
						apiReview, err := convert.ToPullReview(review, ctx.User) | 
				
			||||||
 | 
						if err != nil { | 
				
			||||||
 | 
							ctx.Error(http.StatusInternalServerError, "convertToPullReview", err) | 
				
			||||||
 | 
							return | 
				
			||||||
 | 
						} | 
				
			||||||
 | 
						ctx.JSON(http.StatusOK, apiReview) | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// preparePullReviewType return ReviewType and false or nil and true if an error happen
 | 
				
			||||||
 | 
					func preparePullReviewType(ctx *context.APIContext, pr *models.PullRequest, event api.ReviewStateType, body string) (models.ReviewType, bool) { | 
				
			||||||
 | 
						if err := pr.LoadIssue(); err != nil { | 
				
			||||||
 | 
							ctx.Error(http.StatusInternalServerError, "LoadIssue", err) | 
				
			||||||
 | 
							return -1, true | 
				
			||||||
 | 
						} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var reviewType models.ReviewType | 
				
			||||||
 | 
						switch event { | 
				
			||||||
 | 
						case api.ReviewStateApproved: | 
				
			||||||
 | 
							// can not approve your own PR
 | 
				
			||||||
 | 
							if pr.Issue.IsPoster(ctx.User.ID) { | 
				
			||||||
 | 
								ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("approve your own pull is not allowed")) | 
				
			||||||
 | 
								return -1, true | 
				
			||||||
 | 
							} | 
				
			||||||
 | 
							reviewType = models.ReviewTypeApprove | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						case api.ReviewStateRequestChanges: | 
				
			||||||
 | 
							// can not reject your own PR
 | 
				
			||||||
 | 
							if pr.Issue.IsPoster(ctx.User.ID) { | 
				
			||||||
 | 
								ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("reject your own pull is not allowed")) | 
				
			||||||
 | 
								return -1, true | 
				
			||||||
 | 
							} | 
				
			||||||
 | 
							reviewType = models.ReviewTypeReject | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						case api.ReviewStateComment: | 
				
			||||||
 | 
							reviewType = models.ReviewTypeComment | 
				
			||||||
 | 
						default: | 
				
			||||||
 | 
							reviewType = models.ReviewTypePending | 
				
			||||||
 | 
						} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// reject reviews with empty body if not approve type
 | 
				
			||||||
 | 
						if reviewType != models.ReviewTypeApprove && len(strings.TrimSpace(body)) == 0 { | 
				
			||||||
 | 
							ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("review event %s need body", event)) | 
				
			||||||
 | 
							return -1, true | 
				
			||||||
 | 
						} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return reviewType, false | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// prepareSingleReview return review, related pull and false or nil, nil and true if an error happen
 | 
				
			||||||
 | 
					func prepareSingleReview(ctx *context.APIContext) (*models.Review, *models.PullRequest, bool) { | 
				
			||||||
 | 
						pr, err := models.GetPullRequestByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) | 
				
			||||||
 | 
						if err != nil { | 
				
			||||||
 | 
							if models.IsErrPullRequestNotExist(err) { | 
				
			||||||
 | 
								ctx.NotFound("GetPullRequestByIndex", err) | 
				
			||||||
 | 
							} else { | 
				
			||||||
 | 
								ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err) | 
				
			||||||
 | 
							} | 
				
			||||||
 | 
							return nil, nil, true | 
				
			||||||
 | 
						} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						review, err := models.GetReviewByID(ctx.ParamsInt64(":id")) | 
				
			||||||
 | 
						if err != nil { | 
				
			||||||
 | 
							if models.IsErrReviewNotExist(err) { | 
				
			||||||
 | 
								ctx.NotFound("GetReviewByID", err) | 
				
			||||||
 | 
							} else { | 
				
			||||||
 | 
								ctx.Error(http.StatusInternalServerError, "GetReviewByID", err) | 
				
			||||||
 | 
							} | 
				
			||||||
 | 
							return nil, nil, true | 
				
			||||||
 | 
						} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// validate the the review is for the given PR
 | 
				
			||||||
 | 
						if review.IssueID != pr.IssueID { | 
				
			||||||
 | 
							ctx.NotFound("ReviewNotInPR") | 
				
			||||||
 | 
							return nil, nil, true | 
				
			||||||
 | 
						} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// make sure that the user has access to this review if it is pending
 | 
				
			||||||
 | 
						if review.Type == models.ReviewTypePending && review.ReviewerID != ctx.User.ID && !ctx.User.IsAdmin { | 
				
			||||||
 | 
							ctx.NotFound("GetReviewByID") | 
				
			||||||
 | 
							return nil, nil, true | 
				
			||||||
 | 
						} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := review.LoadAttributes(); err != nil && !models.IsErrUserNotExist(err) { | 
				
			||||||
 | 
							ctx.Error(http.StatusInternalServerError, "ReviewLoadAttributes", err) | 
				
			||||||
 | 
							return nil, nil, true | 
				
			||||||
 | 
						} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return review, pr, false | 
				
			||||||
 | 
					} | 
				
			||||||
					Loading…
					
					
				
		Reference in new issue