* 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>tags/v1.21.12.1
| @@ -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) | |||
| } | |||
| @@ -8,7 +8,7 @@ | |||
| base_repo_id: 1 | |||
| head_branch: branch1 | |||
| base_branch: master | |||
| merge_base: 1234567890abcdef | |||
| merge_base: 4a357436d925b5c974181ff12a994538ddc5a269 | |||
| has_merged: true | |||
| merger_id: 2 | |||
| @@ -22,7 +22,7 @@ | |||
| base_repo_id: 1 | |||
| head_branch: branch2 | |||
| base_branch: master | |||
| merge_base: fedcba9876543210 | |||
| merge_base: 4a357436d925b5c974181ff12a994538ddc5a269 | |||
| has_merged: false | |||
| - | |||
| @@ -60,6 +60,8 @@ | |||
| reviewer_id: 4 | |||
| issue_id: 3 | |||
| content: "New review 5" | |||
| commit_id: 8091a55037cd59e47293aca02981b5a67076b364 | |||
| stale: true | |||
| updated_unix: 946684813 | |||
| created_unix: 946684813 | |||
| - | |||
| @@ -77,5 +79,6 @@ | |||
| reviewer_id: 100 | |||
| issue_id: 3 | |||
| content: "a deleted user's review" | |||
| official: true | |||
| updated_unix: 946684815 | |||
| created_unix: 946684815 | |||
| created_unix: 946684815 | |||
| @@ -74,9 +74,13 @@ type Review struct { | |||
| } | |||
| func (r *Review) loadCodeComments(e Engine) (err error) { | |||
| if r.CodeComments == nil { | |||
| r.CodeComments, err = fetchCodeCommentsByReview(e, r.Issue, nil, r) | |||
| if r.CodeComments != nil { | |||
| return | |||
| } | |||
| if err = r.loadIssue(e); err != nil { | |||
| return | |||
| } | |||
| r.CodeComments, err = fetchCodeCommentsByReview(e, r.Issue, nil, r) | |||
| return | |||
| } | |||
| @@ -86,12 +90,15 @@ func (r *Review) LoadCodeComments() error { | |||
| } | |||
| func (r *Review) loadIssue(e Engine) (err error) { | |||
| if r.Issue != nil { | |||
| return | |||
| } | |||
| r.Issue, err = getIssueByID(e, r.IssueID) | |||
| return | |||
| } | |||
| func (r *Review) loadReviewer(e Engine) (err error) { | |||
| if r.ReviewerID == 0 { | |||
| if r.Reviewer != nil || r.ReviewerID == 0 { | |||
| return nil | |||
| } | |||
| r.Reviewer, err = getUserByID(e, r.ReviewerID) | |||
| @@ -104,10 +111,13 @@ func (r *Review) LoadReviewer() error { | |||
| } | |||
| func (r *Review) loadAttributes(e Engine) (err error) { | |||
| if err = r.loadReviewer(e); err != nil { | |||
| if err = r.loadIssue(e); err != nil { | |||
| return | |||
| } | |||
| if err = r.loadIssue(e); err != nil { | |||
| if err = r.loadCodeComments(e); err != nil { | |||
| return | |||
| } | |||
| if err = r.loadReviewer(e); err != nil { | |||
| return | |||
| } | |||
| return | |||
| @@ -136,6 +146,7 @@ func GetReviewByID(id int64) (*Review, error) { | |||
| // FindReviewOptions represent possible filters to find reviews | |||
| type FindReviewOptions struct { | |||
| ListOptions | |||
| Type ReviewType | |||
| IssueID int64 | |||
| ReviewerID int64 | |||
| @@ -162,6 +173,9 @@ func (opts *FindReviewOptions) toCond() builder.Cond { | |||
| func findReviews(e Engine, opts FindReviewOptions) ([]*Review, error) { | |||
| reviews := make([]*Review, 0, 10) | |||
| sess := e.Where(opts.toCond()) | |||
| if opts.Page > 0 { | |||
| sess = opts.ListOptions.setSessionPagination(sess) | |||
| } | |||
| return reviews, sess. | |||
| Asc("created_unix"). | |||
| Asc("id"). | |||
| @@ -656,3 +670,77 @@ func CanMarkConversation(issue *Issue, doer *User) (permResult bool, err error) | |||
| return true, nil | |||
| } | |||
| // DeleteReview delete a review and it's code comments | |||
| func DeleteReview(r *Review) error { | |||
| sess := x.NewSession() | |||
| defer sess.Close() | |||
| if err := sess.Begin(); err != nil { | |||
| return err | |||
| } | |||
| if r.ID == 0 { | |||
| return fmt.Errorf("review is not allowed to be 0") | |||
| } | |||
| opts := FindCommentsOptions{ | |||
| Type: CommentTypeCode, | |||
| IssueID: r.IssueID, | |||
| ReviewID: r.ID, | |||
| } | |||
| if _, err := sess.Where(opts.toConds()).Delete(new(Comment)); err != nil { | |||
| return err | |||
| } | |||
| opts = FindCommentsOptions{ | |||
| Type: CommentTypeReview, | |||
| IssueID: r.IssueID, | |||
| ReviewID: r.ID, | |||
| } | |||
| if _, err := sess.Where(opts.toConds()).Delete(new(Comment)); err != nil { | |||
| return err | |||
| } | |||
| if _, err := sess.ID(r.ID).Delete(new(Review)); err != nil { | |||
| return err | |||
| } | |||
| return sess.Commit() | |||
| } | |||
| // GetCodeCommentsCount return count of CodeComments a Review has | |||
| func (r *Review) GetCodeCommentsCount() int { | |||
| opts := FindCommentsOptions{ | |||
| Type: CommentTypeCode, | |||
| IssueID: r.IssueID, | |||
| ReviewID: r.ID, | |||
| } | |||
| conds := opts.toConds() | |||
| if r.ID == 0 { | |||
| conds = conds.And(builder.Eq{"invalidated": false}) | |||
| } | |||
| count, err := x.Where(conds).Count(new(Comment)) | |||
| if err != nil { | |||
| return 0 | |||
| } | |||
| return int(count) | |||
| } | |||
| // HTMLURL formats a URL-string to the related review issue-comment | |||
| func (r *Review) HTMLURL() string { | |||
| opts := FindCommentsOptions{ | |||
| Type: CommentTypeReview, | |||
| IssueID: r.IssueID, | |||
| ReviewID: r.ID, | |||
| } | |||
| comment := new(Comment) | |||
| has, err := x.Where(opts.toConds()).Get(comment) | |||
| if err != nil || !has { | |||
| return "" | |||
| } | |||
| return comment.HTMLURL() | |||
| } | |||
| @@ -131,9 +131,11 @@ func TestGetReviewersByIssueID(t *testing.T) { | |||
| allReviews, err := GetReviewersByIssueID(issue.ID) | |||
| assert.NoError(t, err) | |||
| for i, review := range allReviews { | |||
| assert.Equal(t, expectedReviews[i].Reviewer, review.Reviewer) | |||
| assert.Equal(t, expectedReviews[i].Type, review.Type) | |||
| assert.Equal(t, expectedReviews[i].UpdatedUnix, review.UpdatedUnix) | |||
| if assert.Len(t, allReviews, 3) { | |||
| for i, review := range allReviews { | |||
| assert.Equal(t, expectedReviews[i].Reviewer, review.Reviewer) | |||
| assert.Equal(t, expectedReviews[i].Type, review.Type) | |||
| assert.Equal(t, expectedReviews[i].UpdatedUnix, review.UpdatedUnix) | |||
| } | |||
| } | |||
| } | |||
| @@ -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"` | |||
| } | |||
| @@ -500,7 +500,7 @@ func RegisterRoutes(m *macaron.Macaron) { | |||
| bind := binding.Bind | |||
| if setting.API.EnableSwagger { | |||
| m.Get("/swagger", misc.Swagger) //Render V1 by default | |||
| m.Get("/swagger", misc.Swagger) // Render V1 by default | |||
| } | |||
| m.Group("/v1", func() { | |||
| @@ -794,6 +794,20 @@ func RegisterRoutes(m *macaron.Macaron) { | |||
| Patch(reqToken(), reqRepoWriter(models.UnitTypePullRequests), bind(api.EditPullRequestOption{}), repo.EditPullRequest) | |||
| m.Combo("/merge").Get(repo.IsPullRequestMerged). | |||
| Post(reqToken(), mustNotBeArchived, bind(auth.MergePullRequestForm{}), repo.MergePullRequest) | |||
| m.Group("/reviews", func() { | |||
| m.Combo(""). | |||
| Get(repo.ListPullReviews). | |||
| Post(reqToken(), bind(api.CreatePullReviewOptions{}), repo.CreatePullReview) | |||
| m.Group("/:id", func() { | |||
| m.Combo(""). | |||
| Get(repo.GetPullReview). | |||
| Delete(reqToken(), repo.DeletePullReview). | |||
| Post(reqToken(), bind(api.SubmitPullReviewOptions{}), repo.SubmitPullReview) | |||
| m.Combo("/comments"). | |||
| Get(repo.GetPullReviewComments) | |||
| }) | |||
| }) | |||
| }) | |||
| }, mustAllowPulls, reqRepoReader(models.UnitTypeCode), context.ReferencesGitRepo(false)) | |||
| m.Group("/statuses", func() { | |||
| @@ -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 | |||
| } | |||
| @@ -137,4 +137,13 @@ type swaggerParameterBodies struct { | |||
| // in:body | |||
| CreateOAuth2ApplicationOptions api.CreateOAuth2ApplicationOptions | |||
| // in:body | |||
| CreatePullReviewOptions api.CreatePullReviewOptions | |||
| // in:body | |||
| CreatePullReviewComment api.CreatePullReviewComment | |||
| // in:body | |||
| SubmitPullReviewOptions api.SubmitPullReviewOptions | |||
| } | |||
| @@ -141,6 +141,34 @@ type swaggerResponsePullRequestList struct { | |||
| Body []api.PullRequest `json:"body"` | |||
| } | |||
| // PullReview | |||
| // swagger:response PullReview | |||
| type swaggerResponsePullReview struct { | |||
| // in:body | |||
| Body api.PullReview `json:"body"` | |||
| } | |||
| // PullReviewList | |||
| // swagger:response PullReviewList | |||
| type swaggerResponsePullReviewList struct { | |||
| // in:body | |||
| Body []api.PullReview `json:"body"` | |||
| } | |||
| // PullComment | |||
| // swagger:response PullReviewComment | |||
| type swaggerPullReviewComment struct { | |||
| // in:body | |||
| Body api.PullReviewComment `json:"body"` | |||
| } | |||
| // PullCommentList | |||
| // swagger:response PullReviewCommentList | |||
| type swaggerResponsePullReviewCommentList struct { | |||
| // in:body | |||
| Body []api.PullReviewComment `json:"body"` | |||
| } | |||
| // Status | |||
| // swagger:response Status | |||
| type swaggerResponseStatus struct { | |||
| @@ -172,35 +200,35 @@ type swaggerResponseSearchResults struct { | |||
| // AttachmentList | |||
| // swagger:response AttachmentList | |||
| type swaggerResponseAttachmentList struct { | |||
| //in: body | |||
| // in: body | |||
| Body []api.Attachment `json:"body"` | |||
| } | |||
| // Attachment | |||
| // swagger:response Attachment | |||
| type swaggerResponseAttachment struct { | |||
| //in: body | |||
| // in: body | |||
| Body api.Attachment `json:"body"` | |||
| } | |||
| // GitTreeResponse | |||
| // swagger:response GitTreeResponse | |||
| type swaggerGitTreeResponse struct { | |||
| //in: body | |||
| // in: body | |||
| Body api.GitTreeResponse `json:"body"` | |||
| } | |||
| // GitBlobResponse | |||
| // swagger:response GitBlobResponse | |||
| type swaggerGitBlobResponse struct { | |||
| //in: body | |||
| // in: body | |||
| Body api.GitBlobResponse `json:"body"` | |||
| } | |||
| // Commit | |||
| // swagger:response Commit | |||
| type swaggerCommit struct { | |||
| //in: body | |||
| // in: body | |||
| Body api.Commit `json:"body"` | |||
| } | |||
| @@ -222,28 +250,28 @@ type swaggerCommitList struct { | |||
| // True if there is another page | |||
| HasMore bool `json:"X-HasMore"` | |||
| //in: body | |||
| // in: body | |||
| Body []api.Commit `json:"body"` | |||
| } | |||
| // EmptyRepository | |||
| // swagger:response EmptyRepository | |||
| type swaggerEmptyRepository struct { | |||
| //in: body | |||
| // in: body | |||
| Body api.APIError `json:"body"` | |||
| } | |||
| // FileResponse | |||
| // swagger:response FileResponse | |||
| type swaggerFileResponse struct { | |||
| //in: body | |||
| // in: body | |||
| Body api.FileResponse `json:"body"` | |||
| } | |||
| // ContentsResponse | |||
| // swagger:response ContentsResponse | |||
| type swaggerContentsResponse struct { | |||
| //in: body | |||
| // in: body | |||
| Body api.ContentsResponse `json:"body"` | |||
| } | |||
| @@ -257,20 +285,20 @@ type swaggerContentsListResponse struct { | |||
| // FileDeleteResponse | |||
| // swagger:response FileDeleteResponse | |||
| type swaggerFileDeleteResponse struct { | |||
| //in: body | |||
| // in: body | |||
| Body api.FileDeleteResponse `json:"body"` | |||
| } | |||
| // TopicListResponse | |||
| // swagger:response TopicListResponse | |||
| type swaggerTopicListResponse struct { | |||
| //in: body | |||
| // in: body | |||
| Body []api.TopicResponse `json:"body"` | |||
| } | |||
| // TopicNames | |||
| // swagger:response TopicNames | |||
| type swaggerTopicNames struct { | |||
| //in: body | |||
| // in: body | |||
| Body api.TopicName `json:"body"` | |||
| } | |||
| @@ -6714,6 +6714,333 @@ | |||
| } | |||
| } | |||
| }, | |||
| "/repos/{owner}/{repo}/pulls/{index}/reviews": { | |||
| "get": { | |||
| "produces": [ | |||
| "application/json" | |||
| ], | |||
| "tags": [ | |||
| "repository" | |||
| ], | |||
| "summary": "List all reviews for a pull request", | |||
| "operationId": "repoListPullReviews", | |||
| "parameters": [ | |||
| { | |||
| "type": "string", | |||
| "description": "owner of the repo", | |||
| "name": "owner", | |||
| "in": "path", | |||
| "required": true | |||
| }, | |||
| { | |||
| "type": "string", | |||
| "description": "name of the repo", | |||
| "name": "repo", | |||
| "in": "path", | |||
| "required": true | |||
| }, | |||
| { | |||
| "type": "integer", | |||
| "format": "int64", | |||
| "description": "index of the pull request", | |||
| "name": "index", | |||
| "in": "path", | |||
| "required": true | |||
| }, | |||
| { | |||
| "type": "integer", | |||
| "description": "page number of results to return (1-based)", | |||
| "name": "page", | |||
| "in": "query" | |||
| }, | |||
| { | |||
| "type": "integer", | |||
| "description": "page size of results, maximum page size is 50", | |||
| "name": "limit", | |||
| "in": "query" | |||
| } | |||
| ], | |||
| "responses": { | |||
| "200": { | |||
| "$ref": "#/responses/PullReviewList" | |||
| }, | |||
| "404": { | |||
| "$ref": "#/responses/notFound" | |||
| } | |||
| } | |||
| }, | |||
| "post": { | |||
| "produces": [ | |||
| "application/json" | |||
| ], | |||
| "tags": [ | |||
| "repository" | |||
| ], | |||
| "summary": "Create a review to an pull request", | |||
| "operationId": "repoCreatePullReview", | |||
| "parameters": [ | |||
| { | |||
| "type": "string", | |||
| "description": "owner of the repo", | |||
| "name": "owner", | |||
| "in": "path", | |||
| "required": true | |||
| }, | |||
| { | |||
| "type": "string", | |||
| "description": "name of the repo", | |||
| "name": "repo", | |||
| "in": "path", | |||
| "required": true | |||
| }, | |||
| { | |||
| "type": "integer", | |||
| "format": "int64", | |||
| "description": "index of the pull request", | |||
| "name": "index", | |||
| "in": "path", | |||
| "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" | |||
| } | |||
| } | |||
| } | |||
| }, | |||
| "/repos/{owner}/{repo}/pulls/{index}/reviews/{id}": { | |||
| "get": { | |||
| "produces": [ | |||
| "application/json" | |||
| ], | |||
| "tags": [ | |||
| "repository" | |||
| ], | |||
| "summary": "Get a specific review for a pull request", | |||
| "operationId": "repoGetPullReview", | |||
| "parameters": [ | |||
| { | |||
| "type": "string", | |||
| "description": "owner of the repo", | |||
| "name": "owner", | |||
| "in": "path", | |||
| "required": true | |||
| }, | |||
| { | |||
| "type": "string", | |||
| "description": "name of the repo", | |||
| "name": "repo", | |||
| "in": "path", | |||
| "required": true | |||
| }, | |||
| { | |||
| "type": "integer", | |||
| "format": "int64", | |||
| "description": "index of the pull request", | |||
| "name": "index", | |||
| "in": "path", | |||
| "required": true | |||
| }, | |||
| { | |||
| "type": "integer", | |||
| "format": "int64", | |||
| "description": "id of the review", | |||
| "name": "id", | |||
| "in": "path", | |||
| "required": true | |||
| } | |||
| ], | |||
| "responses": { | |||
| "200": { | |||
| "$ref": "#/responses/PullReview" | |||
| }, | |||
| "404": { | |||
| "$ref": "#/responses/notFound" | |||
| } | |||
| } | |||
| }, | |||
| "post": { | |||
| "produces": [ | |||
| "application/json" | |||
| ], | |||
| "tags": [ | |||
| "repository" | |||
| ], | |||
| "summary": "Submit a pending review to an pull request", | |||
| "operationId": "repoSubmitPullReview", | |||
| "parameters": [ | |||
| { | |||
| "type": "string", | |||
| "description": "owner of the repo", | |||
| "name": "owner", | |||
| "in": "path", | |||
| "required": true | |||
| }, | |||
| { | |||
| "type": "string", | |||
| "description": "name of the repo", | |||
| "name": "repo", | |||
| "in": "path", | |||
| "required": true | |||
| }, | |||
| { | |||
| "type": "integer", | |||
| "format": "int64", | |||
| "description": "index of the pull request", | |||
| "name": "index", | |||
| "in": "path", | |||
| "required": true | |||
| }, | |||
| { | |||
| "type": "integer", | |||
| "format": "int64", | |||
| "description": "id of the review", | |||
| "name": "id", | |||
| "in": "path", | |||
| "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" | |||
| } | |||
| } | |||
| }, | |||
| "delete": { | |||
| "produces": [ | |||
| "application/json" | |||
| ], | |||
| "tags": [ | |||
| "repository" | |||
| ], | |||
| "summary": "Delete a specific review from a pull request", | |||
| "operationId": "repoDeletePullReview", | |||
| "parameters": [ | |||
| { | |||
| "type": "string", | |||
| "description": "owner of the repo", | |||
| "name": "owner", | |||
| "in": "path", | |||
| "required": true | |||
| }, | |||
| { | |||
| "type": "string", | |||
| "description": "name of the repo", | |||
| "name": "repo", | |||
| "in": "path", | |||
| "required": true | |||
| }, | |||
| { | |||
| "type": "integer", | |||
| "format": "int64", | |||
| "description": "index of the pull request", | |||
| "name": "index", | |||
| "in": "path", | |||
| "required": true | |||
| }, | |||
| { | |||
| "type": "integer", | |||
| "format": "int64", | |||
| "description": "id of the review", | |||
| "name": "id", | |||
| "in": "path", | |||
| "required": true | |||
| } | |||
| ], | |||
| "responses": { | |||
| "204": { | |||
| "$ref": "#/responses/empty" | |||
| }, | |||
| "403": { | |||
| "$ref": "#/responses/forbidden" | |||
| }, | |||
| "404": { | |||
| "$ref": "#/responses/notFound" | |||
| } | |||
| } | |||
| } | |||
| }, | |||
| "/repos/{owner}/{repo}/pulls/{index}/reviews/{id}/comments": { | |||
| "get": { | |||
| "produces": [ | |||
| "application/json" | |||
| ], | |||
| "tags": [ | |||
| "repository" | |||
| ], | |||
| "summary": "Get a specific review for a pull request", | |||
| "operationId": "repoGetPullReviewComments", | |||
| "parameters": [ | |||
| { | |||
| "type": "string", | |||
| "description": "owner of the repo", | |||
| "name": "owner", | |||
| "in": "path", | |||
| "required": true | |||
| }, | |||
| { | |||
| "type": "string", | |||
| "description": "name of the repo", | |||
| "name": "repo", | |||
| "in": "path", | |||
| "required": true | |||
| }, | |||
| { | |||
| "type": "integer", | |||
| "format": "int64", | |||
| "description": "index of the pull request", | |||
| "name": "index", | |||
| "in": "path", | |||
| "required": true | |||
| }, | |||
| { | |||
| "type": "integer", | |||
| "format": "int64", | |||
| "description": "id of the review", | |||
| "name": "id", | |||
| "in": "path", | |||
| "required": true | |||
| } | |||
| ], | |||
| "responses": { | |||
| "200": { | |||
| "$ref": "#/responses/PullReviewCommentList" | |||
| }, | |||
| "404": { | |||
| "$ref": "#/responses/notFound" | |||
| } | |||
| } | |||
| } | |||
| }, | |||
| "/repos/{owner}/{repo}/raw/{filepath}": { | |||
| "get": { | |||
| "produces": [ | |||
| @@ -10975,6 +11302,59 @@ | |||
| }, | |||
| "x-go-package": "code.gitea.io/gitea/modules/structs" | |||
| }, | |||
| "CreatePullReviewComment": { | |||
| "description": "CreatePullReviewComment represent a review comment for creation api", | |||
| "type": "object", | |||
| "properties": { | |||
| "body": { | |||
| "type": "string", | |||
| "x-go-name": "Body" | |||
| }, | |||
| "new_position": { | |||
| "description": "if comment to new file line or 0", | |||
| "type": "integer", | |||
| "format": "int64", | |||
| "x-go-name": "NewLineNum" | |||
| }, | |||
| "old_position": { | |||
| "description": "if comment to old file line or 0", | |||
| "type": "integer", | |||
| "format": "int64", | |||
| "x-go-name": "OldLineNum" | |||
| }, | |||
| "path": { | |||
| "description": "the tree path", | |||
| "type": "string", | |||
| "x-go-name": "Path" | |||
| } | |||
| }, | |||
| "x-go-package": "code.gitea.io/gitea/modules/structs" | |||
| }, | |||
| "CreatePullReviewOptions": { | |||
| "description": "CreatePullReviewOptions are options to create a pull review", | |||
| "type": "object", | |||
| "properties": { | |||
| "body": { | |||
| "type": "string", | |||
| "x-go-name": "Body" | |||
| }, | |||
| "comments": { | |||
| "type": "array", | |||
| "items": { | |||
| "$ref": "#/definitions/CreatePullReviewComment" | |||
| }, | |||
| "x-go-name": "Comments" | |||
| }, | |||
| "commit_id": { | |||
| "type": "string", | |||
| "x-go-name": "CommitID" | |||
| }, | |||
| "event": { | |||
| "$ref": "#/definitions/ReviewStateType" | |||
| } | |||
| }, | |||
| "x-go-package": "code.gitea.io/gitea/modules/structs" | |||
| }, | |||
| "CreateReleaseOption": { | |||
| "description": "CreateReleaseOption options when creating a release", | |||
| "type": "object", | |||
| @@ -13143,6 +13523,126 @@ | |||
| }, | |||
| "x-go-package": "code.gitea.io/gitea/modules/structs" | |||
| }, | |||
| "PullReview": { | |||
| "description": "PullReview represents a pull request review", | |||
| "type": "object", | |||
| "properties": { | |||
| "body": { | |||
| "type": "string", | |||
| "x-go-name": "Body" | |||
| }, | |||
| "comments_count": { | |||
| "type": "integer", | |||
| "format": "int64", | |||
| "x-go-name": "CodeCommentsCount" | |||
| }, | |||
| "commit_id": { | |||
| "type": "string", | |||
| "x-go-name": "CommitID" | |||
| }, | |||
| "html_url": { | |||
| "type": "string", | |||
| "x-go-name": "HTMLURL" | |||
| }, | |||
| "id": { | |||
| "type": "integer", | |||
| "format": "int64", | |||
| "x-go-name": "ID" | |||
| }, | |||
| "official": { | |||
| "type": "boolean", | |||
| "x-go-name": "Official" | |||
| }, | |||
| "pull_request_url": { | |||
| "type": "string", | |||
| "x-go-name": "HTMLPullURL" | |||
| }, | |||
| "stale": { | |||
| "type": "boolean", | |||
| "x-go-name": "Stale" | |||
| }, | |||
| "state": { | |||
| "$ref": "#/definitions/ReviewStateType" | |||
| }, | |||
| "submitted_at": { | |||
| "type": "string", | |||
| "format": "date-time", | |||
| "x-go-name": "Submitted" | |||
| }, | |||
| "user": { | |||
| "$ref": "#/definitions/User" | |||
| } | |||
| }, | |||
| "x-go-package": "code.gitea.io/gitea/modules/structs" | |||
| }, | |||
| "PullReviewComment": { | |||
| "description": "PullReviewComment represents a comment on a pull request review", | |||
| "type": "object", | |||
| "properties": { | |||
| "body": { | |||
| "type": "string", | |||
| "x-go-name": "Body" | |||
| }, | |||
| "commit_id": { | |||
| "type": "string", | |||
| "x-go-name": "CommitID" | |||
| }, | |||
| "created_at": { | |||
| "type": "string", | |||
| "format": "date-time", | |||
| "x-go-name": "Created" | |||
| }, | |||
| "diff_hunk": { | |||
| "type": "string", | |||
| "x-go-name": "DiffHunk" | |||
| }, | |||
| "html_url": { | |||
| "type": "string", | |||
| "x-go-name": "HTMLURL" | |||
| }, | |||
| "id": { | |||
| "type": "integer", | |||
| "format": "int64", | |||
| "x-go-name": "ID" | |||
| }, | |||
| "original_commit_id": { | |||
| "type": "string", | |||
| "x-go-name": "OrigCommitID" | |||
| }, | |||
| "original_position": { | |||
| "type": "integer", | |||
| "format": "uint64", | |||
| "x-go-name": "OldLineNum" | |||
| }, | |||
| "path": { | |||
| "type": "string", | |||
| "x-go-name": "Path" | |||
| }, | |||
| "position": { | |||
| "type": "integer", | |||
| "format": "uint64", | |||
| "x-go-name": "LineNum" | |||
| }, | |||
| "pull_request_review_id": { | |||
| "type": "integer", | |||
| "format": "int64", | |||
| "x-go-name": "ReviewID" | |||
| }, | |||
| "pull_request_url": { | |||
| "type": "string", | |||
| "x-go-name": "HTMLPullURL" | |||
| }, | |||
| "updated_at": { | |||
| "type": "string", | |||
| "format": "date-time", | |||
| "x-go-name": "Updated" | |||
| }, | |||
| "user": { | |||
| "$ref": "#/definitions/User" | |||
| } | |||
| }, | |||
| "x-go-package": "code.gitea.io/gitea/modules/structs" | |||
| }, | |||
| "Reaction": { | |||
| "description": "Reaction contain one reaction", | |||
| "type": "object", | |||
| @@ -13486,6 +13986,11 @@ | |||
| }, | |||
| "x-go-package": "code.gitea.io/gitea/modules/structs" | |||
| }, | |||
| "ReviewStateType": { | |||
| "description": "ReviewStateType review state type", | |||
| "type": "string", | |||
| "x-go-package": "code.gitea.io/gitea/modules/structs" | |||
| }, | |||
| "SearchResults": { | |||
| "description": "SearchResults results of a successful search", | |||
| "type": "object", | |||
| @@ -13586,6 +14091,20 @@ | |||
| }, | |||
| "x-go-package": "code.gitea.io/gitea/modules/structs" | |||
| }, | |||
| "SubmitPullReviewOptions": { | |||
| "description": "SubmitPullReviewOptions are options to submit a pending pull review", | |||
| "type": "object", | |||
| "properties": { | |||
| "body": { | |||
| "type": "string", | |||
| "x-go-name": "Body" | |||
| }, | |||
| "event": { | |||
| "$ref": "#/definitions/ReviewStateType" | |||
| } | |||
| }, | |||
| "x-go-package": "code.gitea.io/gitea/modules/structs" | |||
| }, | |||
| "Tag": { | |||
| "description": "Tag represents a repository tag", | |||
| "type": "object", | |||
| @@ -14324,6 +14843,36 @@ | |||
| } | |||
| } | |||
| }, | |||
| "PullReview": { | |||
| "description": "PullReview", | |||
| "schema": { | |||
| "$ref": "#/definitions/PullReview" | |||
| } | |||
| }, | |||
| "PullReviewComment": { | |||
| "description": "PullComment", | |||
| "schema": { | |||
| "$ref": "#/definitions/PullReviewComment" | |||
| } | |||
| }, | |||
| "PullReviewCommentList": { | |||
| "description": "PullCommentList", | |||
| "schema": { | |||
| "type": "array", | |||
| "items": { | |||
| "$ref": "#/definitions/PullReviewComment" | |||
| } | |||
| } | |||
| }, | |||
| "PullReviewList": { | |||
| "description": "PullReviewList", | |||
| "schema": { | |||
| "type": "array", | |||
| "items": { | |||
| "$ref": "#/definitions/PullReview" | |||
| } | |||
| } | |||
| }, | |||
| "Reaction": { | |||
| "description": "Reaction", | |||
| "schema": { | |||
| @@ -14561,7 +15110,7 @@ | |||
| "parameterBodies": { | |||
| "description": "parameterBodies", | |||
| "schema": { | |||
| "$ref": "#/definitions/CreateOAuth2ApplicationOptions" | |||
| "$ref": "#/definitions/SubmitPullReviewOptions" | |||
| } | |||
| }, | |||
| "redirect": { | |||