diff --git a/modules/structs/pull_review.go b/modules/structs/pull_review.go new file mode 100644 index 0000000000000..977a96f9599f9 --- /dev/null +++ b/modules/structs/pull_review.go @@ -0,0 +1,40 @@ +// 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" +) + +// PullRequestReview represents a pull request review +type PullRequestReview struct { + ID int64 `json:"id"` + PRURL string `json:"pull_request_url"` + Reviewer *User `json:"user"` + Body string `json:"body"` + CommitID string `json:"commit_id"` + Type string `json:"type"` + + // swagger:strfmt date-time + Created time.Time `json:"created_at"` + + // TODO: is there a way to get a URL to the review itself? + // HTMLURL string `json:"html_url"` +} + +// PullRequestReviewComment represents a comment on a pull request +type PullRequestReviewComment struct { + ID int64 `json:"id"` + URL string `json:"url"` + PRURL string `json:"pull_request_url"` + ReviewID int64 `json:"pull_request_review_id"` + Path string `json:"path"` + CommitID string `json:"commit_id"` + DiffHunk string `json:"diff_hunk"` + LineNum uint64 `json:"position"` + OldLineNum uint64 `json:"original_position"` + Reviewer *User `json:"user"` + Body string `json:"body"` +} diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 4c9f9dd03e2e7..c830ad77aacc5 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() { @@ -773,6 +773,14 @@ func RegisterRoutes(m *macaron.Macaron) { Patch(reqToken(), reqRepoWriter(models.UnitTypePullRequests), bind(api.EditPullRequestOption{}), repo.EditPullRequest) m.Combo("/merge").Get(repo.IsPullRequestMerged). Post(reqToken(), mustNotBeArchived, reqRepoWriter(models.UnitTypePullRequests), bind(auth.MergePullRequestForm{}), repo.MergePullRequest) + m.Group("/reviews", func() { + m.Combo("").Get(repo.ListPullReviews) + m.Group("/:id", func() { + m.Combo("").Get(repo.GetPullReview) + 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 0000000000000..6a1050b7d857b --- /dev/null +++ b/routers/api/v1/repo/pull_review.go @@ -0,0 +1,345 @@ +// 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 ( + "net/http" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/structs" +) + +// 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 + // 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, "LoadIssue", err) + return + } + + allReviews, err := models.FindReviews(models.FindReviewOptions{ + Type: models.ReviewTypeUnknown, + IssueID: pr.IssueID, + }) + + if err != nil { + ctx.Error(http.StatusInternalServerError, "FindReviews", err) + return + } + + var apiReviews []structs.PullRequestReview + for _, review := range allReviews { + // show pending reviews only for the user who created them + if review.Type == models.ReviewTypePending && review.ReviewerID != ctx.User.ID { + continue + } + + if err = review.LoadReviewer(); err != nil { + ctx.Error(http.StatusInternalServerError, "LoadReviewer", err) + return + } + + var reviewType string + switch review.Type { + case models.ReviewTypeApprove: + reviewType = "APPROVE" + case models.ReviewTypeReject: + reviewType = "REJECT" + case models.ReviewTypeComment: + reviewType = "COMMENT" + case models.ReviewTypePending: + reviewType = "PENDING" + default: + reviewType = "UNKNOWN" + + } + + apiReviews = append(apiReviews, structs.PullRequestReview{ + ID: review.ID, + PRURL: pr.Issue.APIURL(), + Reviewer: review.Reviewer.APIFormat(), + Body: review.Content, + Created: review.CreatedUnix.AsTime(), + CommitID: review.CommitID, + Type: reviewType, + }) + } + + 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" + + 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 + } + + 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 + } + + // validate the the review is for the given PR + if review.IssueID != pr.IssueID { + ctx.NotFound("ReviewNotInPR", err) + return + } + + // 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.NotFound("GetReviewByID", 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, "LoadIssue", err) + return + } + + if err = review.LoadReviewer(); err != nil { + ctx.Error(http.StatusInternalServerError, "LoadReviewer", err) + return + } + + var reviewType string + switch review.Type { + case models.ReviewTypeApprove: + reviewType = "APPROVE" + case models.ReviewTypeReject: + reviewType = "REJECT" + case models.ReviewTypeComment: + reviewType = "COMMENT" + case models.ReviewTypePending: + reviewType = "PENDING" + default: + reviewType = "UNKNOWN" + + } + apiReview := structs.PullRequestReview{ + ID: review.ID, + PRURL: pr.Issue.APIURL(), + Reviewer: review.Reviewer.APIFormat(), + Body: review.Content, + Created: review.CreatedUnix.AsTime(), + CommitID: review.CommitID, + Type: reviewType, + } + + 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" + + 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 + } + + 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 + } + + // validate the the review is for the given PR + if review.IssueID != pr.IssueID { + ctx.NotFound("ReviewNotInPR", err) + return + } + + // 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.NotFound("GetReviewByID", err) + return + } + + err = pr.LoadIssue() + if err != nil { + ctx.Error(http.StatusInternalServerError, "LoadIssue", err) + return + } + + err = review.LoadAttributes() + if err != nil { + ctx.Error(http.StatusInternalServerError, "LoadAttributes", err) + return + } + + err = review.LoadCodeComments() + if err != nil { + ctx.Error(http.StatusInternalServerError, "LoadCodeComments", err) + return + } + + err = review.Issue.LoadRepo() + if err != nil { + ctx.Error(http.StatusInternalServerError, "LoadRepo", err) + return + } + + var apiComments []structs.PullRequestReviewComment + + for _, lines := range review.CodeComments { + for _, comments := range lines { + for _, comment := range comments { + apiComment := structs.PullRequestReviewComment{ + ID: comment.ID, + URL: comment.HTMLURL(), + PRURL: review.Issue.APIURL(), + ReviewID: review.ID, + Path: comment.TreePath, + CommitID: comment.CommitSHA, + DiffHunk: comment.Patch, + Reviewer: review.Reviewer.APIFormat(), + Body: comment.Content, + } + if comment.Line < 0 { + apiComment.OldLineNum = comment.UnsignedLine() + } else { + apiComment.LineNum = comment.UnsignedLine() + } + apiComments = append(apiComments, apiComment) + } + } + } + + ctx.JSON(http.StatusOK, &apiComments) +} diff --git a/routers/api/v1/swagger/repo.go b/routers/api/v1/swagger/repo.go index 4ac5c6d2d50b5..0b7acb6453f78 100644 --- a/routers/api/v1/swagger/repo.go +++ b/routers/api/v1/swagger/repo.go @@ -127,6 +127,34 @@ type swaggerResponsePullRequestList struct { Body []api.PullRequest `json:"body"` } +// PullReview +// swagger:response PullReview +type swaggerResponsePullReview struct { + // in:body + Body api.PullRequestReview `json:"body"` +} + +// PullReviewList +// swagger:response PullReviewList +type swaggerResponsePullReviewList struct { + // in:body + Body []api.PullRequestReview `json:"body"` +} + +// PullComment +// swagger:response PullReviewComment +type swaggerPullReviewComment struct { + // in:body + Body api.PullRequestReviewComment `json:"body"` +} + +// PullCommentList +// swagger:response PullReviewCommentList +type swaggerResponsePullReviewCommentList struct { + // in:body + Body []api.PullRequestReviewComment `json:"body"` +} + // Status // swagger:response Status type swaggerResponseStatus struct { @@ -158,35 +186,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"` } @@ -208,28 +236,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"` } @@ -243,20 +271,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 11af8e035bc68..07042e99a97ea 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -6143,6 +6143,154 @@ } } }, + "/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 + } + ], + "responses": { + "200": { + "$ref": "#/responses/PullReviewList" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + } + }, + "/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" + } + } + } + }, + "/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": [ @@ -11968,6 +12116,92 @@ }, "x-go-package": "code.gitea.io/gitea/modules/structs" }, + "PullRequestReview": { + "description": "PullRequestReview represents 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" + }, + "id": { + "type": "integer", + "format": "int64", + "x-go-name": "ID" + }, + "pull_request_url": { + "type": "string", + "x-go-name": "PRURL" + }, + "type": { + "type": "string", + "x-go-name": "Type" + }, + "user": { + "$ref": "#/definitions/User" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, + "PullRequestReviewComment": { + "description": "PullRequestReviewComment represents a comment on a pull request", + "type": "object", + "properties": { + "body": { + "type": "string", + "x-go-name": "Body" + }, + "commit_id": { + "type": "string", + "x-go-name": "CommitID" + }, + "diff_hunk": { + "type": "string", + "x-go-name": "DiffHunk" + }, + "id": { + "type": "integer", + "format": "int64", + "x-go-name": "ID" + }, + "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": "PRURL" + }, + "user": { + "$ref": "#/definitions/User" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, "Reaction": { "description": "Reaction contain one reaction", "type": "object", @@ -13088,6 +13322,36 @@ } } }, + "PullReview": { + "description": "PullReview", + "schema": { + "$ref": "#/definitions/PullRequestReview" + } + }, + "PullReviewComment": { + "description": "PullComment", + "schema": { + "$ref": "#/definitions/PullRequestReviewComment" + } + }, + "PullReviewCommentList": { + "description": "PullCommentList", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/PullRequestReviewComment" + } + } + }, + "PullReviewList": { + "description": "PullReviewList", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/PullRequestReview" + } + } + }, "Reaction": { "description": "Reaction", "schema": {