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