aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
author6543 <6543@obermui.de>2020-05-02 02:20:51 +0200
committerGitHub <noreply@github.com>2020-05-02 03:20:51 +0300
commitc97494a4f4a0e8ba5453e293bcebb76274062b99 (patch)
tree29511446fbdc0296f77390a6566d4ad1eef4668c
parent4ed7d2a2bbb421e37cc0bc23a9f997e5606258ea (diff)
downloadgitea-c97494a4f4a0e8ba5453e293bcebb76274062b99.tar.gz
gitea-c97494a4f4a0e8ba5453e293bcebb76274062b99.zip
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 :ghost: * 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>
-rw-r--r--integrations/api_pull_review_test.go120
-rw-r--r--models/fixtures/pull_request.yml4
-rw-r--r--models/fixtures/review.yml5
-rw-r--r--models/review.go98
-rw-r--r--models/review_test.go10
-rw-r--r--modules/convert/pull_review.go127
-rw-r--r--modules/structs/pull_review.go92
-rw-r--r--routers/api/v1/api.go16
-rw-r--r--routers/api/v1/repo/pull_review.go522
-rw-r--r--routers/api/v1/swagger/options.go9
-rw-r--r--routers/api/v1/swagger/repo.go52
-rw-r--r--templates/swagger/v1_json.tmpl551
12 files changed, 1580 insertions, 26 deletions
diff --git a/integrations/api_pull_review_test.go b/integrations/api_pull_review_test.go
new file mode 100644
index 0000000000..c90a5c11cd
--- /dev/null
+++ b/integrations/api_pull_review_test.go
@@ -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)
+}
diff --git a/models/fixtures/pull_request.yml b/models/fixtures/pull_request.yml
index da9566bc48..b555da83ef 100644
--- a/models/fixtures/pull_request.yml
+++ b/models/fixtures/pull_request.yml
@@ -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
-
diff --git a/models/fixtures/review.yml b/models/fixtures/review.yml
index 87621012ff..35d3dee2e6 100644
--- a/models/fixtures/review.yml
+++ b/models/fixtures/review.yml
@@ -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 \ No newline at end of file
+ created_unix: 946684815
diff --git a/models/review.go b/models/review.go
index c6203e7097..522fe5886c 100644
--- a/models/review.go
+++ b/models/review.go
@@ -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()
+}
diff --git a/models/review_test.go b/models/review_test.go
index 45ddb3181d..fb3a5488e5 100644
--- a/models/review_test.go
+++ b/models/review_test.go
@@ -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)
+ }
}
}
diff --git a/modules/convert/pull_review.go b/modules/convert/pull_review.go
new file mode 100644
index 0000000000..619f9f070e
--- /dev/null
+++ b/modules/convert/pull_review.go
@@ -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 ""
+}
diff --git a/modules/structs/pull_review.go b/modules/structs/pull_review.go
new file mode 100644
index 0000000000..bf9eafc243
--- /dev/null
+++ b/modules/structs/pull_review.go
@@ -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"`
+}
diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go
index cd451c1d5b..754e146fc1 100644
--- a/routers/api/v1/api.go
+++ b/routers/api/v1/api.go
@@ -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() {
diff --git a/routers/api/v1/repo/pull_review.go b/routers/api/v1/repo/pull_review.go
new file mode 100644
index 0000000000..b3772b00a9
--- /dev/null
+++ b/routers/api/v1/repo/pull_review.go
@@ -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
+}
diff --git a/routers/api/v1/swagger/options.go b/routers/api/v1/swagger/options.go
index 4bb649616a..f13dc63864 100644
--- a/routers/api/v1/swagger/options.go
+++ b/routers/api/v1/swagger/options.go
@@ -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
}
diff --git a/routers/api/v1/swagger/repo.go b/routers/api/v1/swagger/repo.go
index 2a657f3122..bcbc2b5fa9 100644
--- a/routers/api/v1/swagger/repo.go
+++ b/routers/api/v1/swagger/repo.go
@@ -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"`
}
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl
index b6a9085be5..01ad43a904 100644
--- a/templates/swagger/v1_json.tmpl
+++ b/templates/swagger/v1_json.tmpl
@@ -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": {