* 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>tags/v1.13.0-dev
@@ -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": { |