2020-05-02 08:20:51 +08:00
|
|
|
// Copyright 2020 The Gitea Authors. All rights reserved.
|
2022-11-28 02:20:29 +08:00
|
|
|
// SPDX-License-Identifier: MIT
|
2020-05-02 08:20:51 +08:00
|
|
|
|
|
|
|
package repo
|
|
|
|
|
|
|
|
import (
|
2024-04-27 19:55:03 +08:00
|
|
|
"errors"
|
2020-05-02 08:20:51 +08:00
|
|
|
"fmt"
|
|
|
|
"net/http"
|
|
|
|
"strings"
|
|
|
|
|
2022-06-13 17:37:59 +08:00
|
|
|
issues_model "code.gitea.io/gitea/models/issues"
|
2022-03-29 14:29:02 +08:00
|
|
|
"code.gitea.io/gitea/models/organization"
|
2022-05-11 18:09:36 +08:00
|
|
|
access_model "code.gitea.io/gitea/models/perm/access"
|
2021-11-24 17:49:20 +08:00
|
|
|
user_model "code.gitea.io/gitea/models/user"
|
Simplify how git repositories are opened (#28937)
## Purpose
This is a refactor toward building an abstraction over managing git
repositories.
Afterwards, it does not matter anymore if they are stored on the local
disk or somewhere remote.
## What this PR changes
We used `git.OpenRepository` everywhere previously.
Now, we should split them into two distinct functions:
Firstly, there are temporary repositories which do not change:
```go
git.OpenRepository(ctx, diskPath)
```
Gitea managed repositories having a record in the database in the
`repository` table are moved into the new package `gitrepo`:
```go
gitrepo.OpenRepository(ctx, repo_model.Repo)
```
Why is `repo_model.Repository` the second parameter instead of file
path?
Because then we can easily adapt our repository storage strategy.
The repositories can be stored locally, however, they could just as well
be stored on a remote server.
## Further changes in other PRs
- A Git Command wrapper on package `gitrepo` could be created. i.e.
`NewCommand(ctx, repo_model.Repository, commands...)`. `git.RunOpts{Dir:
repo.RepoPath()}`, the directory should be empty before invoking this
method and it can be filled in the function only. #28940
- Remove the `RepoPath()`/`WikiPath()` functions to reduce the
possibility of mistakes.
---------
Co-authored-by: delvh <dev.lh@web.de>
2024-01-28 04:09:51 +08:00
|
|
|
"code.gitea.io/gitea/modules/gitrepo"
|
2020-05-02 08:20:51 +08:00
|
|
|
api "code.gitea.io/gitea/modules/structs"
|
2021-01-26 23:36:53 +08:00
|
|
|
"code.gitea.io/gitea/modules/web"
|
2020-05-02 08:20:51 +08:00
|
|
|
"code.gitea.io/gitea/routers/api/v1/utils"
|
2024-02-27 15:12:22 +08:00
|
|
|
"code.gitea.io/gitea/services/context"
|
2022-12-29 10:57:15 +08:00
|
|
|
"code.gitea.io/gitea/services/convert"
|
2020-10-21 02:18:25 +08:00
|
|
|
issue_service "code.gitea.io/gitea/services/issue"
|
2020-05-02 08:20:51 +08:00
|
|
|
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
|
2020-06-09 12:57:38 +08:00
|
|
|
// description: page size of results
|
2020-05-02 08:20:51 +08:00
|
|
|
// type: integer
|
|
|
|
// responses:
|
|
|
|
// "200":
|
|
|
|
// "$ref": "#/responses/PullReviewList"
|
|
|
|
// "404":
|
|
|
|
// "$ref": "#/responses/notFound"
|
|
|
|
|
2022-06-13 17:37:59 +08:00
|
|
|
pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
|
2020-05-02 08:20:51 +08:00
|
|
|
if err != nil {
|
2022-06-13 17:37:59 +08:00
|
|
|
if issues_model.IsErrPullRequestNotExist(err) {
|
2020-05-02 08:20:51 +08:00
|
|
|
ctx.NotFound("GetPullRequestByIndex", err)
|
|
|
|
} else {
|
|
|
|
ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err)
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-11-19 16:12:33 +08:00
|
|
|
if err = pr.LoadIssue(ctx); err != nil {
|
2020-05-02 08:20:51 +08:00
|
|
|
ctx.Error(http.StatusInternalServerError, "LoadIssue", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-04-08 17:11:15 +08:00
|
|
|
if err = pr.Issue.LoadRepo(ctx); err != nil {
|
2020-05-02 08:20:51 +08:00
|
|
|
ctx.Error(http.StatusInternalServerError, "LoadRepo", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-06-13 17:37:59 +08:00
|
|
|
opts := issues_model.FindReviewOptions{
|
2020-05-02 08:20:51 +08:00
|
|
|
ListOptions: utils.GetListOptions(ctx),
|
|
|
|
IssueID: pr.IssueID,
|
2021-08-12 20:43:08 +08:00
|
|
|
}
|
|
|
|
|
2022-06-13 17:37:59 +08:00
|
|
|
allReviews, err := issues_model.FindReviews(ctx, opts)
|
2021-08-12 20:43:08 +08:00
|
|
|
if err != nil {
|
|
|
|
ctx.InternalServerError(err)
|
|
|
|
return
|
|
|
|
}
|
2020-05-02 08:20:51 +08:00
|
|
|
|
2023-09-25 21:17:37 +08:00
|
|
|
count, err := issues_model.CountReviews(ctx, opts)
|
2020-05-02 08:20:51 +08:00
|
|
|
if err != nil {
|
2021-08-12 20:43:08 +08:00
|
|
|
ctx.InternalServerError(err)
|
2020-05-02 08:20:51 +08:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-03-22 15:03:22 +08:00
|
|
|
apiReviews, err := convert.ToPullReviewList(ctx, allReviews, ctx.Doer)
|
2020-05-02 08:20:51 +08:00
|
|
|
if err != nil {
|
|
|
|
ctx.Error(http.StatusInternalServerError, "convertToPullReviewList", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-08-12 20:43:08 +08:00
|
|
|
ctx.SetTotalCountHeader(count)
|
2020-05-02 08:20:51 +08:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2022-03-22 15:03:22 +08:00
|
|
|
apiReview, err := convert.ToPullReview(ctx, review, ctx.Doer)
|
2020-05-02 08:20:51 +08:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2022-03-22 15:03:22 +08:00
|
|
|
apiComments, err := convert.ToPullReviewCommentList(ctx, review, ctx.Doer)
|
2020-05-02 08:20:51 +08:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2022-03-22 15:03:22 +08:00
|
|
|
if ctx.Doer == nil {
|
2020-05-02 08:20:51 +08:00
|
|
|
ctx.NotFound()
|
|
|
|
return
|
|
|
|
}
|
2022-03-22 15:03:22 +08:00
|
|
|
if !ctx.Doer.IsAdmin && ctx.Doer.ID != review.ReviewerID {
|
2020-05-02 08:20:51 +08:00
|
|
|
ctx.Error(http.StatusForbidden, "only admin and user itself can delete a review", nil)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-09-29 20:12:54 +08:00
|
|
|
if err := issues_model.DeleteReview(ctx, review); err != nil {
|
2020-05-02 08:20:51 +08:00
|
|
|
ctx.Error(http.StatusInternalServerError, "DeleteReview", fmt.Errorf("can not delete ReviewID: %d", review.ID))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
ctx.Status(http.StatusNoContent)
|
|
|
|
}
|
|
|
|
|
2023-03-04 15:13:37 +08:00
|
|
|
// CreatePullReview create a review to a pull request
|
2021-01-26 23:36:53 +08:00
|
|
|
func CreatePullReview(ctx *context.APIContext) {
|
2020-05-02 08:20:51 +08:00
|
|
|
// 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"
|
|
|
|
|
2021-01-26 23:36:53 +08:00
|
|
|
opts := web.GetForm(ctx).(*api.CreatePullReviewOptions)
|
2022-06-13 17:37:59 +08:00
|
|
|
pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
|
2020-05-02 08:20:51 +08:00
|
|
|
if err != nil {
|
2022-06-13 17:37:59 +08:00
|
|
|
if issues_model.IsErrPullRequestNotExist(err) {
|
2020-05-02 08:20:51 +08:00
|
|
|
ctx.NotFound("GetPullRequestByIndex", err)
|
|
|
|
} else {
|
|
|
|
ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err)
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// determine review type
|
2021-06-25 06:05:51 +08:00
|
|
|
reviewType, isWrong := preparePullReviewType(ctx, pr, opts.Event, opts.Body, len(opts.Comments) > 0)
|
2020-05-02 08:20:51 +08:00
|
|
|
if isWrong {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-04-08 17:11:15 +08:00
|
|
|
if err := pr.Issue.LoadRepo(ctx); err != nil {
|
2020-05-02 08:20:51 +08:00
|
|
|
ctx.Error(http.StatusInternalServerError, "pr.Issue.LoadRepo", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2020-05-09 02:50:23 +08:00
|
|
|
// if CommitID is empty, set it as lastCommitID
|
|
|
|
if opts.CommitID == "" {
|
Simplify how git repositories are opened (#28937)
## Purpose
This is a refactor toward building an abstraction over managing git
repositories.
Afterwards, it does not matter anymore if they are stored on the local
disk or somewhere remote.
## What this PR changes
We used `git.OpenRepository` everywhere previously.
Now, we should split them into two distinct functions:
Firstly, there are temporary repositories which do not change:
```go
git.OpenRepository(ctx, diskPath)
```
Gitea managed repositories having a record in the database in the
`repository` table are moved into the new package `gitrepo`:
```go
gitrepo.OpenRepository(ctx, repo_model.Repo)
```
Why is `repo_model.Repository` the second parameter instead of file
path?
Because then we can easily adapt our repository storage strategy.
The repositories can be stored locally, however, they could just as well
be stored on a remote server.
## Further changes in other PRs
- A Git Command wrapper on package `gitrepo` could be created. i.e.
`NewCommand(ctx, repo_model.Repository, commands...)`. `git.RunOpts{Dir:
repo.RepoPath()}`, the directory should be empty before invoking this
method and it can be filled in the function only. #28940
- Remove the `RepoPath()`/`WikiPath()` functions to reduce the
possibility of mistakes.
---------
Co-authored-by: delvh <dev.lh@web.de>
2024-01-28 04:09:51 +08:00
|
|
|
gitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, pr.Issue.Repo)
|
2020-05-09 02:50:23 +08:00
|
|
|
if err != nil {
|
2020-09-21 04:20:14 +08:00
|
|
|
ctx.Error(http.StatusInternalServerError, "git.OpenRepository", err)
|
2020-05-09 02:50:23 +08:00
|
|
|
return
|
|
|
|
}
|
2022-01-20 07:26:57 +08:00
|
|
|
defer closer.Close()
|
2020-05-09 02:50:23 +08:00
|
|
|
|
|
|
|
headCommitID, err := gitRepo.GetRefCommitID(pr.GetGitRefName())
|
|
|
|
if err != nil {
|
2020-09-21 04:20:14 +08:00
|
|
|
ctx.Error(http.StatusInternalServerError, "GetRefCommitID", err)
|
2020-05-09 02:50:23 +08:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
opts.CommitID = headCommitID
|
|
|
|
}
|
|
|
|
|
2020-05-02 08:20:51 +08:00
|
|
|
// create review comments
|
|
|
|
for _, c := range opts.Comments {
|
|
|
|
line := c.NewLineNum
|
|
|
|
if c.OldLineNum > 0 {
|
|
|
|
line = c.OldLineNum * -1
|
|
|
|
}
|
|
|
|
|
2022-01-20 07:26:57 +08:00
|
|
|
if _, err := pull_service.CreateCodeComment(ctx,
|
2022-03-22 15:03:22 +08:00
|
|
|
ctx.Doer,
|
2020-05-02 08:20:51 +08:00
|
|
|
ctx.Repo.GitRepo,
|
|
|
|
pr.Issue,
|
|
|
|
line,
|
|
|
|
c.Body,
|
|
|
|
c.Path,
|
2023-03-04 15:13:37 +08:00
|
|
|
true, // pending review
|
2020-05-02 08:20:51 +08:00
|
|
|
0, // no reply
|
|
|
|
opts.CommitID,
|
2024-02-25 14:00:55 +08:00
|
|
|
nil,
|
2020-05-02 08:20:51 +08:00
|
|
|
); err != nil {
|
2020-09-21 04:20:14 +08:00
|
|
|
ctx.Error(http.StatusInternalServerError, "CreateCodeComment", err)
|
2020-05-02 08:20:51 +08:00
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// create review and associate all pending review comments
|
2022-03-22 15:03:22 +08:00
|
|
|
review, _, err := pull_service.SubmitReview(ctx, ctx.Doer, ctx.Repo.GitRepo, pr.Issue, reviewType, opts.Body, opts.CommitID, nil)
|
2020-05-02 08:20:51 +08:00
|
|
|
if err != nil {
|
2024-04-27 19:55:03 +08:00
|
|
|
if errors.Is(err, pull_service.ErrSubmitReviewOnClosedPR) {
|
|
|
|
ctx.Error(http.StatusUnprocessableEntity, "", err)
|
|
|
|
} else {
|
|
|
|
ctx.Error(http.StatusInternalServerError, "SubmitReview", err)
|
|
|
|
}
|
2020-05-02 08:20:51 +08:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// convert response
|
2022-03-22 15:03:22 +08:00
|
|
|
apiReview, err := convert.ToPullReview(ctx, review, ctx.Doer)
|
2020-05-02 08:20:51 +08:00
|
|
|
if err != nil {
|
|
|
|
ctx.Error(http.StatusInternalServerError, "convertToPullReview", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
ctx.JSON(http.StatusOK, apiReview)
|
|
|
|
}
|
|
|
|
|
|
|
|
// SubmitPullReview submit a pending review to an pull request
|
2021-01-26 23:36:53 +08:00
|
|
|
func SubmitPullReview(ctx *context.APIContext) {
|
2020-05-02 08:20:51 +08:00
|
|
|
// 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"
|
|
|
|
|
2021-01-26 23:36:53 +08:00
|
|
|
opts := web.GetForm(ctx).(*api.SubmitPullReviewOptions)
|
2020-05-02 08:20:51 +08:00
|
|
|
review, pr, isWrong := prepareSingleReview(ctx)
|
|
|
|
if isWrong {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-06-13 17:37:59 +08:00
|
|
|
if review.Type != issues_model.ReviewTypePending {
|
2020-05-02 08:20:51 +08:00
|
|
|
ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("only a pending review can be submitted"))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// determine review type
|
2021-06-25 06:05:51 +08:00
|
|
|
reviewType, isWrong := preparePullReviewType(ctx, pr, opts.Event, opts.Body, len(review.Comments) > 0)
|
2020-05-02 08:20:51 +08:00
|
|
|
if isWrong {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// if review stay pending return
|
2022-06-13 17:37:59 +08:00
|
|
|
if reviewType == issues_model.ReviewTypePending {
|
2020-05-02 08:20:51 +08:00
|
|
|
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
|
2022-03-22 15:03:22 +08:00
|
|
|
review, _, err = pull_service.SubmitReview(ctx, ctx.Doer, ctx.Repo.GitRepo, pr.Issue, reviewType, opts.Body, headCommitID, nil)
|
2020-05-02 08:20:51 +08:00
|
|
|
if err != nil {
|
2024-04-27 19:55:03 +08:00
|
|
|
if errors.Is(err, pull_service.ErrSubmitReviewOnClosedPR) {
|
|
|
|
ctx.Error(http.StatusUnprocessableEntity, "", err)
|
|
|
|
} else {
|
|
|
|
ctx.Error(http.StatusInternalServerError, "SubmitReview", err)
|
|
|
|
}
|
2020-05-02 08:20:51 +08:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// convert response
|
2022-03-22 15:03:22 +08:00
|
|
|
apiReview, err := convert.ToPullReview(ctx, review, ctx.Doer)
|
2020-05-02 08:20:51 +08:00
|
|
|
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
|
2022-06-13 17:37:59 +08:00
|
|
|
func preparePullReviewType(ctx *context.APIContext, pr *issues_model.PullRequest, event api.ReviewStateType, body string, hasComments bool) (issues_model.ReviewType, bool) {
|
2022-11-19 16:12:33 +08:00
|
|
|
if err := pr.LoadIssue(ctx); err != nil {
|
2020-05-02 08:20:51 +08:00
|
|
|
ctx.Error(http.StatusInternalServerError, "LoadIssue", err)
|
|
|
|
return -1, true
|
|
|
|
}
|
|
|
|
|
2021-06-25 06:05:51 +08:00
|
|
|
needsBody := true
|
|
|
|
hasBody := len(strings.TrimSpace(body)) > 0
|
|
|
|
|
2022-06-13 17:37:59 +08:00
|
|
|
var reviewType issues_model.ReviewType
|
2020-05-02 08:20:51 +08:00
|
|
|
switch event {
|
|
|
|
case api.ReviewStateApproved:
|
|
|
|
// can not approve your own PR
|
2022-03-22 15:03:22 +08:00
|
|
|
if pr.Issue.IsPoster(ctx.Doer.ID) {
|
2020-05-02 08:20:51 +08:00
|
|
|
ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("approve your own pull is not allowed"))
|
|
|
|
return -1, true
|
|
|
|
}
|
2022-06-13 17:37:59 +08:00
|
|
|
reviewType = issues_model.ReviewTypeApprove
|
2021-06-25 06:05:51 +08:00
|
|
|
needsBody = false
|
2020-05-02 08:20:51 +08:00
|
|
|
|
|
|
|
case api.ReviewStateRequestChanges:
|
|
|
|
// can not reject your own PR
|
2022-03-22 15:03:22 +08:00
|
|
|
if pr.Issue.IsPoster(ctx.Doer.ID) {
|
2020-05-02 08:20:51 +08:00
|
|
|
ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("reject your own pull is not allowed"))
|
|
|
|
return -1, true
|
|
|
|
}
|
2022-06-13 17:37:59 +08:00
|
|
|
reviewType = issues_model.ReviewTypeReject
|
2020-05-02 08:20:51 +08:00
|
|
|
|
|
|
|
case api.ReviewStateComment:
|
2022-06-13 17:37:59 +08:00
|
|
|
reviewType = issues_model.ReviewTypeComment
|
2021-06-25 06:05:51 +08:00
|
|
|
needsBody = false
|
|
|
|
// if there is no body we need to ensure that there are comments
|
|
|
|
if !hasBody && !hasComments {
|
|
|
|
ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("review event %s requires a body or a comment", event))
|
|
|
|
return -1, true
|
|
|
|
}
|
2020-05-02 08:20:51 +08:00
|
|
|
default:
|
2022-06-13 17:37:59 +08:00
|
|
|
reviewType = issues_model.ReviewTypePending
|
2020-05-02 08:20:51 +08:00
|
|
|
}
|
|
|
|
|
2021-06-25 06:05:51 +08:00
|
|
|
// reject reviews with empty body if a body is required for this call
|
|
|
|
if needsBody && !hasBody {
|
|
|
|
ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("review event %s requires a body", event))
|
2020-05-02 08:20:51 +08:00
|
|
|
return -1, true
|
|
|
|
}
|
|
|
|
|
|
|
|
return reviewType, false
|
|
|
|
}
|
|
|
|
|
|
|
|
// prepareSingleReview return review, related pull and false or nil, nil and true if an error happen
|
2022-06-13 17:37:59 +08:00
|
|
|
func prepareSingleReview(ctx *context.APIContext) (*issues_model.Review, *issues_model.PullRequest, bool) {
|
|
|
|
pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
|
2020-05-02 08:20:51 +08:00
|
|
|
if err != nil {
|
2022-06-13 17:37:59 +08:00
|
|
|
if issues_model.IsErrPullRequestNotExist(err) {
|
2020-05-02 08:20:51 +08:00
|
|
|
ctx.NotFound("GetPullRequestByIndex", err)
|
|
|
|
} else {
|
|
|
|
ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err)
|
|
|
|
}
|
|
|
|
return nil, nil, true
|
|
|
|
}
|
|
|
|
|
2022-06-13 17:37:59 +08:00
|
|
|
review, err := issues_model.GetReviewByID(ctx, ctx.ParamsInt64(":id"))
|
2020-05-02 08:20:51 +08:00
|
|
|
if err != nil {
|
2022-06-13 17:37:59 +08:00
|
|
|
if issues_model.IsErrReviewNotExist(err) {
|
2020-05-02 08:20:51 +08:00
|
|
|
ctx.NotFound("GetReviewByID", err)
|
|
|
|
} else {
|
|
|
|
ctx.Error(http.StatusInternalServerError, "GetReviewByID", err)
|
|
|
|
}
|
|
|
|
return nil, nil, true
|
|
|
|
}
|
|
|
|
|
2024-03-11 17:24:23 +08:00
|
|
|
// validate the review is for the given PR
|
2020-05-02 08:20:51 +08:00
|
|
|
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
|
2022-06-13 17:37:59 +08:00
|
|
|
if review.Type == issues_model.ReviewTypePending && review.ReviewerID != ctx.Doer.ID && !ctx.Doer.IsAdmin {
|
2020-05-02 08:20:51 +08:00
|
|
|
ctx.NotFound("GetReviewByID")
|
|
|
|
return nil, nil, true
|
|
|
|
}
|
|
|
|
|
2022-01-20 07:26:57 +08:00
|
|
|
if err := review.LoadAttributes(ctx); err != nil && !user_model.IsErrUserNotExist(err) {
|
2020-05-02 08:20:51 +08:00
|
|
|
ctx.Error(http.StatusInternalServerError, "ReviewLoadAttributes", err)
|
|
|
|
return nil, nil, true
|
|
|
|
}
|
|
|
|
|
|
|
|
return review, pr, false
|
|
|
|
}
|
2020-10-21 02:18:25 +08:00
|
|
|
|
|
|
|
// CreateReviewRequests create review requests to an pull request
|
2021-01-26 23:36:53 +08:00
|
|
|
func CreateReviewRequests(ctx *context.APIContext) {
|
2020-10-21 02:18:25 +08:00
|
|
|
// swagger:operation POST /repos/{owner}/{repo}/pulls/{index}/requested_reviewers repository repoCreatePullReviewRequests
|
|
|
|
// ---
|
|
|
|
// summary: create review requests 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: body
|
|
|
|
// in: body
|
|
|
|
// required: true
|
|
|
|
// schema:
|
|
|
|
// "$ref": "#/definitions/PullReviewRequestOptions"
|
|
|
|
// responses:
|
|
|
|
// "201":
|
|
|
|
// "$ref": "#/responses/PullReviewList"
|
|
|
|
// "422":
|
|
|
|
// "$ref": "#/responses/validationError"
|
|
|
|
// "404":
|
|
|
|
// "$ref": "#/responses/notFound"
|
2021-01-26 23:36:53 +08:00
|
|
|
|
|
|
|
opts := web.GetForm(ctx).(*api.PullReviewRequestOptions)
|
|
|
|
apiReviewRequest(ctx, *opts, true)
|
2020-10-21 02:18:25 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// DeleteReviewRequests delete review requests to an pull request
|
2021-01-26 23:36:53 +08:00
|
|
|
func DeleteReviewRequests(ctx *context.APIContext) {
|
2020-10-21 02:18:25 +08:00
|
|
|
// swagger:operation DELETE /repos/{owner}/{repo}/pulls/{index}/requested_reviewers repository repoDeletePullReviewRequests
|
|
|
|
// ---
|
|
|
|
// summary: cancel review requests 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: body
|
|
|
|
// in: body
|
|
|
|
// required: true
|
|
|
|
// schema:
|
|
|
|
// "$ref": "#/definitions/PullReviewRequestOptions"
|
|
|
|
// responses:
|
|
|
|
// "204":
|
|
|
|
// "$ref": "#/responses/empty"
|
|
|
|
// "422":
|
|
|
|
// "$ref": "#/responses/validationError"
|
2024-03-28 23:19:24 +08:00
|
|
|
// "403":
|
|
|
|
// "$ref": "#/responses/forbidden"
|
2020-10-21 02:18:25 +08:00
|
|
|
// "404":
|
|
|
|
// "$ref": "#/responses/notFound"
|
2021-01-26 23:36:53 +08:00
|
|
|
opts := web.GetForm(ctx).(*api.PullReviewRequestOptions)
|
|
|
|
apiReviewRequest(ctx, *opts, false)
|
2020-10-21 02:18:25 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
func apiReviewRequest(ctx *context.APIContext, opts api.PullReviewRequestOptions, isAdd bool) {
|
2022-06-13 17:37:59 +08:00
|
|
|
pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
|
2020-10-21 02:18:25 +08:00
|
|
|
if err != nil {
|
2022-06-13 17:37:59 +08:00
|
|
|
if issues_model.IsErrPullRequestNotExist(err) {
|
2020-10-21 02:18:25 +08:00
|
|
|
ctx.NotFound("GetPullRequestByIndex", err)
|
|
|
|
} else {
|
|
|
|
ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err)
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-04-08 17:11:15 +08:00
|
|
|
if err := pr.Issue.LoadRepo(ctx); err != nil {
|
2020-10-21 02:18:25 +08:00
|
|
|
ctx.Error(http.StatusInternalServerError, "pr.Issue.LoadRepo", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-11-24 17:49:20 +08:00
|
|
|
reviewers := make([]*user_model.User, 0, len(opts.Reviewers))
|
2020-10-21 02:18:25 +08:00
|
|
|
|
2022-05-11 18:09:36 +08:00
|
|
|
permDoer, err := access_model.GetUserRepoPermission(ctx, pr.Issue.Repo, ctx.Doer)
|
2020-10-21 02:18:25 +08:00
|
|
|
if err != nil {
|
|
|
|
ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, r := range opts.Reviewers {
|
2021-11-24 17:49:20 +08:00
|
|
|
var reviewer *user_model.User
|
2020-10-21 02:18:25 +08:00
|
|
|
if strings.Contains(r, "@") {
|
Add context cache as a request level cache (#22294)
To avoid duplicated load of the same data in an HTTP request, we can set
a context cache to do that. i.e. Some pages may load a user from a
database with the same id in different areas on the same page. But the
code is hidden in two different deep logic. How should we share the
user? As a result of this PR, now if both entry functions accept
`context.Context` as the first parameter and we just need to refactor
`GetUserByID` to reuse the user from the context cache. Then it will not
be loaded twice on an HTTP request.
But of course, sometimes we would like to reload an object from the
database, that's why `RemoveContextData` is also exposed.
The core context cache is here. It defines a new context
```go
type cacheContext struct {
ctx context.Context
data map[any]map[any]any
lock sync.RWMutex
}
var cacheContextKey = struct{}{}
func WithCacheContext(ctx context.Context) context.Context {
return context.WithValue(ctx, cacheContextKey, &cacheContext{
ctx: ctx,
data: make(map[any]map[any]any),
})
}
```
Then you can use the below 4 methods to read/write/del the data within
the same context.
```go
func GetContextData(ctx context.Context, tp, key any) any
func SetContextData(ctx context.Context, tp, key, value any)
func RemoveContextData(ctx context.Context, tp, key any)
func GetWithContextCache[T any](ctx context.Context, cacheGroupKey string, cacheTargetID any, f func() (T, error)) (T, error)
```
Then let's take a look at how `system.GetString` implement it.
```go
func GetSetting(ctx context.Context, key string) (string, error) {
return cache.GetWithContextCache(ctx, contextCacheKey, key, func() (string, error) {
return cache.GetString(genSettingCacheKey(key), func() (string, error) {
res, err := GetSettingNoCache(ctx, key)
if err != nil {
return "", err
}
return res.SettingValue, nil
})
})
}
```
First, it will check if context data include the setting object with the
key. If not, it will query from the global cache which may be memory or
a Redis cache. If not, it will get the object from the database. In the
end, if the object gets from the global cache or database, it will be
set into the context cache.
An object stored in the context cache will only be destroyed after the
context disappeared.
2023-02-15 21:37:34 +08:00
|
|
|
reviewer, err = user_model.GetUserByEmail(ctx, r)
|
2020-10-21 02:18:25 +08:00
|
|
|
} else {
|
2022-05-20 22:08:52 +08:00
|
|
|
reviewer, err = user_model.GetUserByName(ctx, r)
|
2020-10-21 02:18:25 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
if err != nil {
|
2021-11-24 17:49:20 +08:00
|
|
|
if user_model.IsErrUserNotExist(err) {
|
2020-10-21 02:18:25 +08:00
|
|
|
ctx.NotFound("UserNotExist", fmt.Sprintf("User '%s' not exist", r))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
ctx.Error(http.StatusInternalServerError, "GetUser", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-04-28 19:48:48 +08:00
|
|
|
err = issue_service.IsValidReviewRequest(ctx, reviewer, ctx.Doer, isAdd, pr.Issue, &permDoer)
|
2020-10-21 02:18:25 +08:00
|
|
|
if err != nil {
|
2022-06-13 17:37:59 +08:00
|
|
|
if issues_model.IsErrNotValidReviewRequest(err) {
|
2020-10-21 02:18:25 +08:00
|
|
|
ctx.Error(http.StatusUnprocessableEntity, "NotValidReviewRequest", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
ctx.Error(http.StatusInternalServerError, "IsValidReviewRequest", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
reviewers = append(reviewers, reviewer)
|
|
|
|
}
|
|
|
|
|
2022-06-13 17:37:59 +08:00
|
|
|
var reviews []*issues_model.Review
|
2020-10-21 02:18:25 +08:00
|
|
|
if isAdd {
|
2022-06-13 17:37:59 +08:00
|
|
|
reviews = make([]*issues_model.Review, 0, len(reviewers))
|
2020-10-21 02:18:25 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
for _, reviewer := range reviewers {
|
2023-04-15 02:18:28 +08:00
|
|
|
comment, err := issue_service.ReviewRequest(ctx, pr.Issue, ctx.Doer, reviewer, isAdd)
|
2020-10-21 02:18:25 +08:00
|
|
|
if err != nil {
|
2024-03-28 23:19:24 +08:00
|
|
|
if issues_model.IsErrReviewRequestOnClosedPR(err) {
|
|
|
|
ctx.Error(http.StatusForbidden, "", err)
|
|
|
|
return
|
|
|
|
}
|
2020-10-21 02:18:25 +08:00
|
|
|
ctx.Error(http.StatusInternalServerError, "ReviewRequest", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if comment != nil && isAdd {
|
2023-09-29 20:12:54 +08:00
|
|
|
if err = comment.LoadReview(ctx); err != nil {
|
2020-10-21 02:18:25 +08:00
|
|
|
ctx.ServerError("ReviewRequest", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
reviews = append(reviews, comment.Review)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if ctx.Repo.Repository.Owner.IsOrganization() && len(opts.TeamReviewers) > 0 {
|
2022-03-29 14:29:02 +08:00
|
|
|
teamReviewers := make([]*organization.Team, 0, len(opts.TeamReviewers))
|
2020-10-21 02:18:25 +08:00
|
|
|
for _, t := range opts.TeamReviewers {
|
2022-03-29 14:29:02 +08:00
|
|
|
var teamReviewer *organization.Team
|
2022-05-20 22:08:52 +08:00
|
|
|
teamReviewer, err = organization.GetTeam(ctx, ctx.Repo.Owner.ID, t)
|
2020-10-21 02:18:25 +08:00
|
|
|
if err != nil {
|
2022-03-29 14:29:02 +08:00
|
|
|
if organization.IsErrTeamNotExist(err) {
|
2020-10-21 02:18:25 +08:00
|
|
|
ctx.NotFound("TeamNotExist", fmt.Sprintf("Team '%s' not exist", t))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
ctx.Error(http.StatusInternalServerError, "ReviewRequest", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-04-28 19:48:48 +08:00
|
|
|
err = issue_service.IsValidTeamReviewRequest(ctx, teamReviewer, ctx.Doer, isAdd, pr.Issue)
|
2020-10-21 02:18:25 +08:00
|
|
|
if err != nil {
|
2022-06-13 17:37:59 +08:00
|
|
|
if issues_model.IsErrNotValidReviewRequest(err) {
|
2020-10-21 02:18:25 +08:00
|
|
|
ctx.Error(http.StatusUnprocessableEntity, "NotValidReviewRequest", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
ctx.Error(http.StatusInternalServerError, "IsValidTeamReviewRequest", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
teamReviewers = append(teamReviewers, teamReviewer)
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, teamReviewer := range teamReviewers {
|
2023-04-15 02:18:28 +08:00
|
|
|
comment, err := issue_service.TeamReviewRequest(ctx, pr.Issue, ctx.Doer, teamReviewer, isAdd)
|
2020-10-21 02:18:25 +08:00
|
|
|
if err != nil {
|
|
|
|
ctx.ServerError("TeamReviewRequest", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if comment != nil && isAdd {
|
2023-09-29 20:12:54 +08:00
|
|
|
if err = comment.LoadReview(ctx); err != nil {
|
2020-10-21 02:18:25 +08:00
|
|
|
ctx.ServerError("ReviewRequest", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
reviews = append(reviews, comment.Review)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if isAdd {
|
2022-03-22 15:03:22 +08:00
|
|
|
apiReviews, err := convert.ToPullReviewList(ctx, reviews, ctx.Doer)
|
2020-10-21 02:18:25 +08:00
|
|
|
if err != nil {
|
|
|
|
ctx.Error(http.StatusInternalServerError, "convertToPullReviewList", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
ctx.JSON(http.StatusCreated, apiReviews)
|
|
|
|
} else {
|
|
|
|
ctx.Status(http.StatusNoContent)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
2021-02-12 01:32:25 +08:00
|
|
|
|
|
|
|
// DismissPullReview dismiss a review for a pull request
|
|
|
|
func DismissPullReview(ctx *context.APIContext) {
|
|
|
|
// swagger:operation POST /repos/{owner}/{repo}/pulls/{index}/reviews/{id}/dismissals repository repoDismissPullReview
|
|
|
|
// ---
|
|
|
|
// summary: Dismiss a 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
|
|
|
|
// - name: body
|
|
|
|
// in: body
|
|
|
|
// required: true
|
|
|
|
// schema:
|
|
|
|
// "$ref": "#/definitions/DismissPullReviewOptions"
|
|
|
|
// responses:
|
|
|
|
// "200":
|
|
|
|
// "$ref": "#/responses/PullReview"
|
|
|
|
// "403":
|
|
|
|
// "$ref": "#/responses/forbidden"
|
2023-09-13 10:37:54 +08:00
|
|
|
// "404":
|
|
|
|
// "$ref": "#/responses/notFound"
|
2021-02-12 01:32:25 +08:00
|
|
|
// "422":
|
|
|
|
// "$ref": "#/responses/validationError"
|
|
|
|
opts := web.GetForm(ctx).(*api.DismissPullReviewOptions)
|
2022-07-19 21:20:28 +08:00
|
|
|
dismissReview(ctx, opts.Message, true, opts.Priors)
|
2021-02-12 01:32:25 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// UnDismissPullReview cancel to dismiss a review for a pull request
|
|
|
|
func UnDismissPullReview(ctx *context.APIContext) {
|
|
|
|
// swagger:operation POST /repos/{owner}/{repo}/pulls/{index}/reviews/{id}/undismissals repository repoUnDismissPullReview
|
|
|
|
// ---
|
|
|
|
// summary: Cancel to dismiss a 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"
|
|
|
|
// "403":
|
|
|
|
// "$ref": "#/responses/forbidden"
|
2023-09-13 10:37:54 +08:00
|
|
|
// "404":
|
|
|
|
// "$ref": "#/responses/notFound"
|
2021-02-12 01:32:25 +08:00
|
|
|
// "422":
|
|
|
|
// "$ref": "#/responses/validationError"
|
2022-07-19 21:20:28 +08:00
|
|
|
dismissReview(ctx, "", false, false)
|
2021-02-12 01:32:25 +08:00
|
|
|
}
|
|
|
|
|
2022-07-19 21:20:28 +08:00
|
|
|
func dismissReview(ctx *context.APIContext, msg string, isDismiss, dismissPriors bool) {
|
2021-02-12 01:32:25 +08:00
|
|
|
if !ctx.Repo.IsAdmin() {
|
|
|
|
ctx.Error(http.StatusForbidden, "", "Must be repo admin")
|
|
|
|
return
|
|
|
|
}
|
2024-03-28 23:19:24 +08:00
|
|
|
review, _, isWrong := prepareSingleReview(ctx)
|
2021-02-12 01:32:25 +08:00
|
|
|
if isWrong {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-06-13 17:37:59 +08:00
|
|
|
if review.Type != issues_model.ReviewTypeApprove && review.Type != issues_model.ReviewTypeReject {
|
2021-02-12 01:32:25 +08:00
|
|
|
ctx.Error(http.StatusForbidden, "", "not need to dismiss this review because it's type is not Approve or change request")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-07-19 21:20:28 +08:00
|
|
|
_, err := pull_service.DismissReview(ctx, review.ID, ctx.Repo.Repository.ID, msg, ctx.Doer, isDismiss, dismissPriors)
|
2021-02-12 01:32:25 +08:00
|
|
|
if err != nil {
|
2024-03-28 23:19:24 +08:00
|
|
|
if pull_service.IsErrDismissRequestOnClosedPR(err) {
|
|
|
|
ctx.Error(http.StatusForbidden, "", err)
|
|
|
|
return
|
|
|
|
}
|
2021-02-12 01:32:25 +08:00
|
|
|
ctx.Error(http.StatusInternalServerError, "pull_service.DismissReview", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-06-13 17:37:59 +08:00
|
|
|
if review, err = issues_model.GetReviewByID(ctx, review.ID); err != nil {
|
2021-02-12 01:32:25 +08:00
|
|
|
ctx.Error(http.StatusInternalServerError, "GetReviewByID", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// convert response
|
2022-03-22 15:03:22 +08:00
|
|
|
apiReview, err := convert.ToPullReview(ctx, review, ctx.Doer)
|
2021-02-12 01:32:25 +08:00
|
|
|
if err != nil {
|
|
|
|
ctx.Error(http.StatusInternalServerError, "convertToPullReview", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
ctx.JSON(http.StatusOK, apiReview)
|
|
|
|
}
|