summaryrefslogtreecommitdiffstats
path: root/routers
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 /routers
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>
Diffstat (limited to 'routers')
-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
4 files changed, 586 insertions, 13 deletions
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"`
}