2016-12-02 14:10:39 +03:00
// Copyright 2016 The Gitea Authors. All rights reserved.
2022-11-27 21:20:29 +03:00
// SPDX-License-Identifier: MIT
2016-12-02 14:10:39 +03:00
package repo
import (
2021-07-13 02:26:25 +03:00
"errors"
2016-12-02 14:10:39 +03:00
"fmt"
2021-07-02 15:19:57 +03:00
"math"
2018-07-18 00:23:58 +03:00
"net/http"
2021-07-02 15:19:57 +03:00
"strconv"
2016-12-02 14:10:39 +03:00
"strings"
2019-11-03 17:46:32 +03:00
"time"
2016-12-02 14:10:39 +03:00
"code.gitea.io/gitea/models"
2022-08-25 05:31:57 +03:00
activities_model "code.gitea.io/gitea/models/activities"
2023-01-16 11:00:22 +03:00
git_model "code.gitea.io/gitea/models/git"
2022-04-08 12:11:15 +03:00
issues_model "code.gitea.io/gitea/models/issues"
2022-05-11 13:09:36 +03:00
access_model "code.gitea.io/gitea/models/perm/access"
2022-05-07 20:05:52 +03:00
pull_model "code.gitea.io/gitea/models/pull"
2021-12-10 04:27:50 +03:00
repo_model "code.gitea.io/gitea/models/repo"
2021-11-09 22:57:58 +03:00
"code.gitea.io/gitea/models/unit"
2021-11-24 12:49:20 +03:00
user_model "code.gitea.io/gitea/models/user"
2016-12-02 14:10:39 +03:00
"code.gitea.io/gitea/modules/context"
2019-03-27 12:33:00 +03:00
"code.gitea.io/gitea/modules/git"
2016-12-02 14:10:39 +03:00
"code.gitea.io/gitea/modules/log"
2020-05-17 00:05:19 +03:00
"code.gitea.io/gitea/modules/notification"
2022-09-29 05:27:20 +03:00
"code.gitea.io/gitea/modules/setting"
2019-05-11 13:21:34 +03:00
api "code.gitea.io/gitea/modules/structs"
2019-08-15 17:46:21 +03:00
"code.gitea.io/gitea/modules/timeutil"
2021-01-26 18:36:53 +03:00
"code.gitea.io/gitea/modules/web"
2020-01-24 22:00:29 +03:00
"code.gitea.io/gitea/routers/api/v1/utils"
2021-12-10 11:14:24 +03:00
asymkey_service "code.gitea.io/gitea/services/asymkey"
2022-05-07 20:05:52 +03:00
"code.gitea.io/gitea/services/automerge"
2022-12-29 05:57:15 +03:00
"code.gitea.io/gitea/services/convert"
2021-04-06 22:44:05 +03:00
"code.gitea.io/gitea/services/forms"
2022-09-29 05:27:20 +03:00
"code.gitea.io/gitea/services/gitdiff"
2019-10-25 17:46:37 +03:00
issue_service "code.gitea.io/gitea/services/issue"
2019-09-27 03:22:36 +03:00
pull_service "code.gitea.io/gitea/services/pull"
2021-07-13 02:26:25 +03:00
repo_service "code.gitea.io/gitea/services/repository"
2016-12-02 14:10:39 +03:00
)
// ListPullRequests returns a list of all PRs
2021-01-26 18:36:53 +03:00
func ListPullRequests ( ctx * context . APIContext ) {
2017-11-13 10:02:25 +03:00
// swagger:operation GET /repos/{owner}/{repo}/pulls repository repoListPullRequests
// ---
// summary: List a repo's pull requests
// 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
2018-10-21 06:40:42 +03:00
// - name: state
// in: query
// description: "State of pull request: open or closed (optional)"
// type: string
// enum: [closed, open, all]
// - name: sort
// in: query
// description: "Type of sort"
// type: string
// enum: [oldest, recentupdate, leastupdate, mostcomment, leastcomment, priority]
// - name: milestone
// in: query
// description: "ID of the milestone"
// type: integer
// format: int64
// - name: labels
// in: query
// description: "Label IDs"
// type: array
// collectionFormat: multi
// items:
// type: integer
// format: int64
2020-01-24 22:00:29 +03:00
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
2020-06-09 07:57:38 +03:00
// description: page size of results
2020-01-24 22:00:29 +03:00
// type: integer
2017-11-13 10:02:25 +03:00
// responses:
// "200":
// "$ref": "#/responses/PullRequestList"
2019-12-20 20:07:12 +03:00
2020-01-24 22:00:29 +03:00
listOptions := utils . GetListOptions ( ctx )
2022-06-13 12:37:59 +03:00
prs , maxResults , err := issues_model . PullRequests ( ctx . Repo . Repository . ID , & issues_model . PullRequestsOptions {
2020-01-24 22:00:29 +03:00
ListOptions : listOptions ,
2021-07-29 04:42:15 +03:00
State : ctx . FormTrim ( "state" ) ,
SortType : ctx . FormTrim ( "sort" ) ,
Labels : ctx . FormStrings ( "labels" ) ,
MilestoneID : ctx . FormInt64 ( "milestone" ) ,
2016-12-02 14:10:39 +03:00
} )
if err != nil {
2019-12-20 20:07:12 +03:00
ctx . Error ( http . StatusInternalServerError , "PullRequests" , err )
2016-12-02 14:10:39 +03:00
return
}
apiPrs := make ( [ ] * api . PullRequest , len ( prs ) )
for i := range prs {
2022-11-19 11:12:33 +03:00
if err = prs [ i ] . LoadIssue ( ctx ) ; err != nil {
2019-12-20 20:07:12 +03:00
ctx . Error ( http . StatusInternalServerError , "LoadIssue" , err )
2017-11-04 21:10:01 +03:00
return
}
2022-11-19 11:12:33 +03:00
if err = prs [ i ] . LoadAttributes ( ctx ) ; err != nil {
2019-12-20 20:07:12 +03:00
ctx . Error ( http . StatusInternalServerError , "LoadAttributes" , err )
2017-11-04 21:10:01 +03:00
return
}
2022-11-19 11:12:33 +03:00
if err = prs [ i ] . LoadBaseRepo ( ctx ) ; err != nil {
2020-03-03 01:31:55 +03:00
ctx . Error ( http . StatusInternalServerError , "LoadBaseRepo" , err )
2017-11-04 21:10:01 +03:00
return
}
2022-11-19 11:12:33 +03:00
if err = prs [ i ] . LoadHeadRepo ( ctx ) ; err != nil {
2020-03-03 01:31:55 +03:00
ctx . Error ( http . StatusInternalServerError , "LoadHeadRepo" , err )
2017-11-04 21:10:01 +03:00
return
}
2022-03-22 10:03:22 +03:00
apiPrs [ i ] = convert . ToAPIPullRequest ( ctx , prs [ i ] , ctx . Doer )
2016-12-02 14:10:39 +03:00
}
2020-01-24 22:00:29 +03:00
ctx . SetLinkHeader ( int ( maxResults ) , listOptions . PageSize )
2021-08-12 15:43:08 +03:00
ctx . SetTotalCountHeader ( maxResults )
2019-12-20 20:07:12 +03:00
ctx . JSON ( http . StatusOK , & apiPrs )
2016-12-02 14:10:39 +03:00
}
// GetPullRequest returns a single PR based on index
func GetPullRequest ( ctx * context . APIContext ) {
2017-11-13 10:02:25 +03:00
// swagger:operation GET /repos/{owner}/{repo}/pulls/{index} repository repoGetPullRequest
// ---
// summary: Get 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 to get
// type: integer
2018-10-21 06:40:42 +03:00
// format: int64
2017-11-13 10:02:25 +03:00
// required: true
// responses:
// "200":
// "$ref": "#/responses/PullRequest"
2020-01-09 14:56:32 +03:00
// "404":
// "$ref": "#/responses/notFound"
2019-12-20 20:07:12 +03:00
2022-06-13 12:37:59 +03:00
pr , err := issues_model . GetPullRequestByIndex ( ctx , ctx . Repo . Repository . ID , ctx . ParamsInt64 ( ":index" ) )
2016-12-02 14:10:39 +03:00
if err != nil {
2022-06-13 12:37:59 +03:00
if issues_model . IsErrPullRequestNotExist ( err ) {
2019-03-19 05:29:43 +03:00
ctx . NotFound ( )
2016-12-02 14:10:39 +03:00
} else {
2019-12-20 20:07:12 +03:00
ctx . Error ( http . StatusInternalServerError , "GetPullRequestByIndex" , err )
2016-12-02 14:10:39 +03:00
}
return
}
2022-11-19 11:12:33 +03:00
if err = pr . LoadBaseRepo ( ctx ) ; err != nil {
2020-03-03 01:31:55 +03:00
ctx . Error ( http . StatusInternalServerError , "LoadBaseRepo" , err )
2017-11-04 21:10:01 +03:00
return
}
2022-11-19 11:12:33 +03:00
if err = pr . LoadHeadRepo ( ctx ) ; err != nil {
2020-03-03 01:31:55 +03:00
ctx . Error ( http . StatusInternalServerError , "LoadHeadRepo" , err )
2017-11-04 21:10:01 +03:00
return
}
2022-03-22 10:03:22 +03:00
ctx . JSON ( http . StatusOK , convert . ToAPIPullRequest ( ctx , pr , ctx . Doer ) )
2016-12-02 14:10:39 +03:00
}
2021-09-22 02:04:53 +03:00
// DownloadPullDiffOrPatch render a pull's raw diff or patch
func DownloadPullDiffOrPatch ( ctx * context . APIContext ) {
// swagger:operation GET /repos/{owner}/{repo}/pulls/{index}.{diffType} repository repoDownloadPullDiffOrPatch
2020-06-05 14:03:12 +03:00
// ---
2021-09-22 02:04:53 +03:00
// summary: Get a pull request diff or patch
2020-06-05 14:03:12 +03:00
// produces:
// - text/plain
// 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 to get
// type: integer
// format: int64
// required: true
2021-09-22 02:04:53 +03:00
// - name: diffType
2020-06-05 14:03:12 +03:00
// in: path
2021-09-22 02:04:53 +03:00
// description: whether the output is diff or patch
2020-06-05 14:03:12 +03:00
// type: string
2021-09-22 02:04:53 +03:00
// enum: [diff, patch]
2020-06-05 14:03:12 +03:00
// required: true
2021-09-28 00:09:49 +03:00
// - name: binary
// in: query
// description: whether to include binary file changes. if true, the diff is applicable with `git apply`
// type: boolean
2020-06-05 14:03:12 +03:00
// responses:
// "200":
// "$ref": "#/responses/string"
// "404":
// "$ref": "#/responses/notFound"
2022-06-13 12:37:59 +03:00
pr , err := issues_model . GetPullRequestByIndex ( ctx , ctx . Repo . Repository . ID , ctx . ParamsInt64 ( ":index" ) )
2020-06-05 14:03:12 +03:00
if err != nil {
2022-06-13 12:37:59 +03:00
if issues_model . IsErrPullRequestNotExist ( err ) {
2020-06-05 14:03:12 +03:00
ctx . NotFound ( )
} else {
ctx . InternalServerError ( err )
}
return
}
2021-09-22 02:04:53 +03:00
var patch bool
if ctx . Params ( ":diffType" ) == "diff" {
patch = false
} else {
patch = true
}
2020-06-05 14:03:12 +03:00
2021-09-28 00:09:49 +03:00
binary := ctx . FormBool ( "binary" )
2022-01-20 02:26:57 +03:00
if err := pull_service . DownloadDiffOrPatch ( ctx , pr , ctx , patch , binary ) ; err != nil {
2020-06-05 14:03:12 +03:00
ctx . InternalServerError ( err )
return
}
}
2016-12-02 14:10:39 +03:00
// CreatePullRequest does what it says
2021-01-26 18:36:53 +03:00
func CreatePullRequest ( ctx * context . APIContext ) {
2017-11-13 10:02:25 +03:00
// swagger:operation POST /repos/{owner}/{repo}/pulls repository repoCreatePullRequest
// ---
// summary: Create a pull request
// consumes:
// - application/json
// 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: body
// in: body
// schema:
// "$ref": "#/definitions/CreatePullRequestOption"
// responses:
// "201":
// "$ref": "#/responses/PullRequest"
2019-12-20 20:07:12 +03:00
// "409":
// "$ref": "#/responses/error"
// "422":
// "$ref": "#/responses/validationError"
2021-01-26 18:36:53 +03:00
form := * web . GetForm ( ctx ) . ( * api . CreatePullRequestOption )
2020-10-26 12:05:27 +03:00
if form . Head == form . Base {
ctx . Error ( http . StatusUnprocessableEntity , "BaseHeadSame" ,
"Invalid PullRequest: There are no changes between the head and the base" )
return
}
2016-12-02 14:10:39 +03:00
var (
repo = ctx . Repo . Repository
labelIDs [ ] int64
milestoneID int64
)
// Get repo/branch information
2019-10-18 14:13:31 +03:00
_ , headRepo , headGitRepo , compareInfo , baseBranch , headBranch := parseCompareInfo ( ctx , form )
2016-12-02 14:10:39 +03:00
if ctx . Written ( ) {
return
}
2019-11-13 10:01:19 +03:00
defer headGitRepo . Close ( )
2016-12-02 14:10:39 +03:00
// Check if another PR exists with the same targets
2022-11-19 11:12:33 +03:00
existingPr , err := issues_model . GetUnmergedPullRequest ( ctx , headRepo . ID , ctx . Repo . Repository . ID , headBranch , baseBranch , issues_model . PullRequestFlowGithub )
2016-12-02 14:10:39 +03:00
if err != nil {
2022-06-13 12:37:59 +03:00
if ! issues_model . IsErrPullRequestNotExist ( err ) {
2019-12-20 20:07:12 +03:00
ctx . Error ( http . StatusInternalServerError , "GetUnmergedPullRequest" , err )
2016-12-02 14:10:39 +03:00
return
}
} else {
2022-06-13 12:37:59 +03:00
err = issues_model . ErrPullRequestAlreadyExists {
2016-12-02 14:10:39 +03:00
ID : existingPr . ID ,
IssueID : existingPr . Index ,
HeadRepoID : existingPr . HeadRepoID ,
BaseRepoID : existingPr . BaseRepoID ,
HeadBranch : existingPr . HeadBranch ,
BaseBranch : existingPr . BaseBranch ,
}
2019-12-20 20:07:12 +03:00
ctx . Error ( http . StatusConflict , "GetUnmergedPullRequest" , err )
2016-12-02 14:10:39 +03:00
return
}
if len ( form . Labels ) > 0 {
2022-11-19 11:12:33 +03:00
labels , err := issues_model . GetLabelsInRepoByIDs ( ctx , ctx . Repo . Repository . ID , form . Labels )
2016-12-02 14:10:39 +03:00
if err != nil {
2019-12-20 20:07:12 +03:00
ctx . Error ( http . StatusInternalServerError , "GetLabelsInRepoByIDs" , err )
2016-12-02 14:10:39 +03:00
return
}
2020-04-20 16:10:45 +03:00
labelIDs = make ( [ ] int64 , len ( form . Labels ) )
orgLabelIDs := make ( [ ] int64 , len ( form . Labels ) )
2016-12-02 14:10:39 +03:00
for i := range labels {
labelIDs [ i ] = labels [ i ] . ID
}
2020-04-20 16:10:45 +03:00
if ctx . Repo . Owner . IsOrganization ( ) {
2022-11-19 11:12:33 +03:00
orgLabels , err := issues_model . GetLabelsInOrgByIDs ( ctx , ctx . Repo . Owner . ID , form . Labels )
2020-04-20 16:10:45 +03:00
if err != nil {
ctx . Error ( http . StatusInternalServerError , "GetLabelsInOrgByIDs" , err )
return
}
for i := range orgLabels {
orgLabelIDs [ i ] = orgLabels [ i ] . ID
}
}
labelIDs = append ( labelIDs , orgLabelIDs ... )
2016-12-02 14:10:39 +03:00
}
if form . Milestone > 0 {
2022-04-08 12:11:15 +03:00
milestone , err := issues_model . GetMilestoneByRepoID ( ctx , ctx . Repo . Repository . ID , form . Milestone )
2016-12-02 14:10:39 +03:00
if err != nil {
2022-04-08 12:11:15 +03:00
if issues_model . IsErrMilestoneNotExist ( err ) {
2019-03-19 05:29:43 +03:00
ctx . NotFound ( )
2016-12-02 14:10:39 +03:00
} else {
2019-12-20 20:07:12 +03:00
ctx . Error ( http . StatusInternalServerError , "GetMilestoneByRepoID" , err )
2016-12-02 14:10:39 +03:00
}
return
}
milestoneID = milestone . ID
}
2019-08-15 17:46:21 +03:00
var deadlineUnix timeutil . TimeStamp
2018-05-01 22:05:28 +03:00
if form . Deadline != nil {
2019-08-15 17:46:21 +03:00
deadlineUnix = timeutil . TimeStamp ( form . Deadline . Unix ( ) )
2018-05-01 22:05:28 +03:00
}
2022-06-13 12:37:59 +03:00
prIssue := & issues_model . Issue {
2018-05-01 22:05:28 +03:00
RepoID : repo . ID ,
Title : form . Title ,
2022-03-22 10:03:22 +03:00
PosterID : ctx . Doer . ID ,
Poster : ctx . Doer ,
2018-05-01 22:05:28 +03:00
MilestoneID : milestoneID ,
IsPull : true ,
Content : form . Body ,
DeadlineUnix : deadlineUnix ,
2016-12-02 14:10:39 +03:00
}
2022-06-13 12:37:59 +03:00
pr := & issues_model . PullRequest {
2019-10-18 14:13:31 +03:00
HeadRepoID : headRepo . ID ,
BaseRepoID : repo . ID ,
HeadBranch : headBranch ,
BaseBranch : baseBranch ,
HeadRepo : headRepo ,
BaseRepo : repo ,
MergeBase : compareInfo . MergeBase ,
2022-06-13 12:37:59 +03:00
Type : issues_model . PullRequestGitea ,
2016-12-02 14:10:39 +03:00
}
2018-05-09 19:29:04 +03:00
// Get all assignee IDs
2022-11-19 11:12:33 +03:00
assigneeIDs , err := issues_model . MakeIDsFromAPIAssigneesToAdd ( ctx , form . Assignee , form . Assignees )
2018-05-09 19:29:04 +03:00
if err != nil {
2021-11-24 12:49:20 +03:00
if user_model . IsErrUserNotExist ( err ) {
2019-12-20 20:07:12 +03:00
ctx . Error ( http . StatusUnprocessableEntity , "" , fmt . Sprintf ( "Assignee does not exist: [name: %s]" , err ) )
2018-05-09 19:29:04 +03:00
} else {
2019-12-20 20:07:12 +03:00
ctx . Error ( http . StatusInternalServerError , "AddAssigneeByName" , err )
2018-05-09 19:29:04 +03:00
}
return
}
2019-10-25 17:46:37 +03:00
// Check if the passed assignees is assignable
for _ , aID := range assigneeIDs {
2022-12-03 05:48:26 +03:00
assignee , err := user_model . GetUserByID ( ctx , aID )
2019-10-25 17:46:37 +03:00
if err != nil {
2019-12-20 20:07:12 +03:00
ctx . Error ( http . StatusInternalServerError , "GetUserByID" , err )
2019-10-25 17:46:37 +03:00
return
}
2018-05-09 19:29:04 +03:00
2022-05-11 13:09:36 +03:00
valid , err := access_model . CanBeAssigned ( ctx , assignee , repo , true )
2019-10-25 17:46:37 +03:00
if err != nil {
2019-12-20 20:07:12 +03:00
ctx . Error ( http . StatusInternalServerError , "canBeAssigned" , err )
2019-10-25 17:46:37 +03:00
return
}
if ! valid {
2022-06-13 12:37:59 +03:00
ctx . Error ( http . StatusUnprocessableEntity , "canBeAssigned" , repo_model . ErrUserDoesNotHaveAccessToRepo { UserID : aID , RepoName : repo . Name } )
2019-10-25 17:46:37 +03:00
return
}
}
2022-01-20 02:26:57 +03:00
if err := pull_service . NewPullRequest ( ctx , repo , prIssue , labelIDs , [ ] string { } , pr , assigneeIDs ) ; err != nil {
2023-03-12 15:28:18 +03:00
if errors . Is ( err , user_model . ErrBlockedByUser ) {
ctx . Error ( http . StatusForbidden , "BlockedByUser" , err )
return
} else if repo_model . IsErrUserDoesNotHaveAccessToRepo ( err ) {
2019-12-20 20:07:12 +03:00
ctx . Error ( http . StatusBadRequest , "UserDoesNotHaveAccessToRepo" , err )
2018-05-09 19:29:04 +03:00
return
}
2019-12-20 20:07:12 +03:00
ctx . Error ( http . StatusInternalServerError , "NewPullRequest" , err )
2016-12-02 14:10:39 +03:00
return
}
log . Trace ( "Pull request created: %d/%d" , repo . ID , prIssue . ID )
2022-03-22 10:03:22 +03:00
ctx . JSON ( http . StatusCreated , convert . ToAPIPullRequest ( ctx , pr , ctx . Doer ) )
2016-12-02 14:10:39 +03:00
}
// EditPullRequest does what it says
2021-01-26 18:36:53 +03:00
func EditPullRequest ( ctx * context . APIContext ) {
2017-11-13 10:02:25 +03:00
// swagger:operation PATCH /repos/{owner}/{repo}/pulls/{index} repository repoEditPullRequest
// ---
2019-11-03 17:46:32 +03:00
// summary: Update a pull request. If using deadline only the date will be taken into account, and time of day ignored.
2017-11-13 10:02:25 +03:00
// consumes:
// - application/json
// 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 to edit
// type: integer
2018-10-21 06:40:42 +03:00
// format: int64
2017-11-13 10:02:25 +03:00
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/EditPullRequestOption"
// responses:
// "201":
// "$ref": "#/responses/PullRequest"
2019-12-20 20:07:12 +03:00
// "403":
// "$ref": "#/responses/forbidden"
2020-06-07 22:13:40 +03:00
// "409":
// "$ref": "#/responses/error"
2019-12-20 20:07:12 +03:00
// "412":
// "$ref": "#/responses/error"
// "422":
// "$ref": "#/responses/validationError"
2021-01-26 18:36:53 +03:00
form := web . GetForm ( ctx ) . ( * api . EditPullRequestOption )
2022-06-13 12:37:59 +03:00
pr , err := issues_model . GetPullRequestByIndex ( ctx , ctx . Repo . Repository . ID , ctx . ParamsInt64 ( ":index" ) )
2016-12-02 14:10:39 +03:00
if err != nil {
2022-06-13 12:37:59 +03:00
if issues_model . IsErrPullRequestNotExist ( err ) {
2019-03-19 05:29:43 +03:00
ctx . NotFound ( )
2016-12-02 14:10:39 +03:00
} else {
2019-12-20 20:07:12 +03:00
ctx . Error ( http . StatusInternalServerError , "GetPullRequestByIndex" , err )
2016-12-02 14:10:39 +03:00
}
return
}
2022-11-19 11:12:33 +03:00
err = pr . LoadIssue ( ctx )
2019-06-12 22:41:28 +03:00
if err != nil {
ctx . Error ( http . StatusInternalServerError , "LoadIssue" , err )
return
}
2016-12-02 14:10:39 +03:00
issue := pr . Issue
2018-12-13 18:55:43 +03:00
issue . Repo = ctx . Repo . Repository
2016-12-02 14:10:39 +03:00
2022-12-22 01:45:44 +03:00
if err := issue . LoadAttributes ( ctx ) ; err != nil {
ctx . Error ( http . StatusInternalServerError , "LoadAttributes" , err )
return
}
2022-03-22 10:03:22 +03:00
if ! issue . IsPoster ( ctx . Doer . ID ) && ! ctx . Repo . CanWrite ( unit . TypePullRequests ) {
2019-12-20 20:07:12 +03:00
ctx . Status ( http . StatusForbidden )
2016-12-02 14:10:39 +03:00
return
}
2020-05-17 00:05:19 +03:00
oldTitle := issue . Title
2016-12-02 14:10:39 +03:00
if len ( form . Title ) > 0 {
issue . Title = form . Title
}
if len ( form . Body ) > 0 {
issue . Content = form . Body
}
2019-11-03 17:46:32 +03:00
// Update or remove deadline if set
if form . Deadline != nil || form . RemoveDeadline != nil {
var deadlineUnix timeutil . TimeStamp
if ( form . RemoveDeadline == nil || ! * form . RemoveDeadline ) && ! form . Deadline . IsZero ( ) {
deadline := time . Date ( form . Deadline . Year ( ) , form . Deadline . Month ( ) , form . Deadline . Day ( ) ,
23 , 59 , 59 , 0 , form . Deadline . Location ( ) )
deadlineUnix = timeutil . TimeStamp ( deadline . Unix ( ) )
}
2022-06-13 12:37:59 +03:00
if err := issues_model . UpdateIssueDeadline ( issue , deadlineUnix , ctx . Doer ) ; err != nil {
2019-12-20 20:07:12 +03:00
ctx . Error ( http . StatusInternalServerError , "UpdateIssueDeadline" , err )
2019-10-28 02:35:20 +03:00
return
}
issue . DeadlineUnix = deadlineUnix
2018-05-01 22:05:28 +03:00
}
2018-05-09 19:29:04 +03:00
// Add/delete assignees
2016-12-02 14:10:39 +03:00
2019-03-10 00:15:45 +03:00
// Deleting is done the GitHub way (quote from their api documentation):
2018-05-09 19:29:04 +03:00
// https://developer.github.com/v3/issues/#edit-an-issue
// "assignees" (array): Logins for Users to assign to this issue.
// Pass one or more user logins to replace the set of assignees on this Issue.
// Send an empty array ([]) to clear all assignees from the Issue.
2021-11-09 22:57:58 +03:00
if ctx . Repo . CanWrite ( unit . TypePullRequests ) && ( form . Assignees != nil || len ( form . Assignee ) > 0 ) {
2023-04-14 21:18:28 +03:00
err = issue_service . UpdateAssignees ( ctx , issue , form . Assignee , form . Assignees , ctx . Doer )
2018-05-09 19:29:04 +03:00
if err != nil {
2021-11-24 12:49:20 +03:00
if user_model . IsErrUserNotExist ( err ) {
2019-12-20 20:07:12 +03:00
ctx . Error ( http . StatusUnprocessableEntity , "" , fmt . Sprintf ( "Assignee does not exist: [name: %s]" , err ) )
2018-05-09 19:29:04 +03:00
} else {
2019-12-20 20:07:12 +03:00
ctx . Error ( http . StatusInternalServerError , "UpdateAssignees" , err )
2018-05-09 19:29:04 +03:00
}
2016-12-02 14:10:39 +03:00
return
}
}
2018-05-09 19:29:04 +03:00
2021-11-09 22:57:58 +03:00
if ctx . Repo . CanWrite ( unit . TypePullRequests ) && form . Milestone != 0 &&
2016-12-02 14:10:39 +03:00
issue . MilestoneID != form . Milestone {
oldMilestoneID := issue . MilestoneID
issue . MilestoneID = form . Milestone
2022-03-22 10:03:22 +03:00
if err = issue_service . ChangeMilestoneAssign ( issue , ctx . Doer , oldMilestoneID ) ; err != nil {
2019-12-20 20:07:12 +03:00
ctx . Error ( http . StatusInternalServerError , "ChangeMilestoneAssign" , err )
2016-12-02 14:10:39 +03:00
return
}
}
2021-11-09 22:57:58 +03:00
if ctx . Repo . CanWrite ( unit . TypePullRequests ) && form . Labels != nil {
2022-11-19 11:12:33 +03:00
labels , err := issues_model . GetLabelsInRepoByIDs ( ctx , ctx . Repo . Repository . ID , form . Labels )
2018-11-16 14:12:44 +03:00
if err != nil {
2019-12-20 20:07:12 +03:00
ctx . Error ( http . StatusInternalServerError , "GetLabelsInRepoByIDsError" , err )
2018-11-16 14:12:44 +03:00
return
}
2020-04-20 16:10:45 +03:00
if ctx . Repo . Owner . IsOrganization ( ) {
2022-11-19 11:12:33 +03:00
orgLabels , err := issues_model . GetLabelsInOrgByIDs ( ctx , ctx . Repo . Owner . ID , form . Labels )
2020-04-20 16:10:45 +03:00
if err != nil {
ctx . Error ( http . StatusInternalServerError , "GetLabelsInOrgByIDs" , err )
return
}
labels = append ( labels , orgLabels ... )
}
2022-06-13 12:37:59 +03:00
if err = issues_model . ReplaceIssueLabels ( issue , labels , ctx . Doer ) ; err != nil {
2019-12-20 20:07:12 +03:00
ctx . Error ( http . StatusInternalServerError , "ReplaceLabelsError" , err )
2018-11-16 14:12:44 +03:00
return
}
}
2016-12-02 14:10:39 +03:00
if form . State != nil {
2021-10-03 06:11:17 +03:00
if pr . HasMerged {
ctx . Error ( http . StatusPreconditionFailed , "MergedPRState" , "cannot change state of this pull request, it was already merged" )
return
}
2021-04-09 10:40:34 +03:00
issue . IsClosed = api . StateClosed == api . StateType ( * form . State )
2020-05-17 00:05:19 +03:00
}
2022-06-13 12:37:59 +03:00
statusChangeComment , titleChanged , err := issues_model . UpdateIssueByAPI ( issue , ctx . Doer )
2020-05-17 00:05:19 +03:00
if err != nil {
2022-06-13 12:37:59 +03:00
if issues_model . IsErrDependenciesLeft ( err ) {
2020-05-17 00:05:19 +03:00
ctx . Error ( http . StatusPreconditionFailed , "DependenciesLeft" , "cannot close this pull request because it still has open dependencies" )
2016-12-02 14:10:39 +03:00
return
}
2020-05-17 00:05:19 +03:00
ctx . Error ( http . StatusInternalServerError , "UpdateIssueByAPI" , err )
return
}
if titleChanged {
2022-11-19 11:12:33 +03:00
notification . NotifyIssueChangeTitle ( ctx , ctx . Doer , issue , oldTitle )
2020-05-17 00:05:19 +03:00
}
if statusChangeComment != nil {
2023-01-25 07:47:53 +03:00
notification . NotifyIssueChangeStatus ( ctx , ctx . Doer , "" , issue , statusChangeComment , issue . IsClosed )
2016-12-02 14:10:39 +03:00
}
2020-06-07 22:13:40 +03:00
// change pull target branch
2021-10-03 06:11:17 +03:00
if ! pr . HasMerged && len ( form . Base ) != 0 && form . Base != pr . BaseBranch {
2020-06-07 22:13:40 +03:00
if ! ctx . Repo . GitRepo . IsBranchExist ( form . Base ) {
ctx . Error ( http . StatusNotFound , "NewBaseBranchNotExist" , fmt . Errorf ( "new base '%s' not exist" , form . Base ) )
return
}
2022-03-22 10:03:22 +03:00
if err := pull_service . ChangeTargetBranch ( ctx , pr , ctx . Doer , form . Base ) ; err != nil {
2022-06-13 12:37:59 +03:00
if issues_model . IsErrPullRequestAlreadyExists ( err ) {
2020-06-07 22:13:40 +03:00
ctx . Error ( http . StatusConflict , "IsErrPullRequestAlreadyExists" , err )
return
2022-06-13 12:37:59 +03:00
} else if issues_model . IsErrIssueIsClosed ( err ) {
2020-06-07 22:13:40 +03:00
ctx . Error ( http . StatusUnprocessableEntity , "IsErrIssueIsClosed" , err )
return
} else if models . IsErrPullRequestHasMerged ( err ) {
ctx . Error ( http . StatusConflict , "IsErrPullRequestHasMerged" , err )
return
} else {
ctx . InternalServerError ( err )
}
return
}
2022-11-19 11:12:33 +03:00
notification . NotifyPullRequestChangeTargetBranch ( ctx , ctx . Doer , pr , form . Base )
2020-06-07 22:13:40 +03:00
}
2022-04-28 18:45:33 +03:00
// update allow edits
if form . AllowMaintainerEdit != nil {
if err := pull_service . SetAllowEdits ( ctx , ctx . Doer , pr , * form . AllowMaintainerEdit ) ; err != nil {
if errors . Is ( pull_service . ErrUserHasNoPermissionForAction , err ) {
ctx . Error ( http . StatusForbidden , "SetAllowEdits" , fmt . Sprintf ( "SetAllowEdits: %s" , err ) )
return
}
ctx . ServerError ( "SetAllowEdits" , err )
return
}
}
2016-12-02 14:10:39 +03:00
// Refetch from database
2022-06-13 12:37:59 +03:00
pr , err = issues_model . GetPullRequestByIndex ( ctx , ctx . Repo . Repository . ID , pr . Index )
2016-12-02 14:10:39 +03:00
if err != nil {
2022-06-13 12:37:59 +03:00
if issues_model . IsErrPullRequestNotExist ( err ) {
2019-03-19 05:29:43 +03:00
ctx . NotFound ( )
2016-12-02 14:10:39 +03:00
} else {
2019-12-20 20:07:12 +03:00
ctx . Error ( http . StatusInternalServerError , "GetPullRequestByIndex" , err )
2016-12-02 14:10:39 +03:00
}
return
}
2017-11-13 10:02:25 +03:00
// TODO this should be 200, not 201
2022-03-22 10:03:22 +03:00
ctx . JSON ( http . StatusCreated , convert . ToAPIPullRequest ( ctx , pr , ctx . Doer ) )
2016-12-02 14:10:39 +03:00
}
// IsPullRequestMerged checks if a PR exists given an index
func IsPullRequestMerged ( ctx * context . APIContext ) {
2017-11-13 10:02:25 +03:00
// swagger:operation GET /repos/{owner}/{repo}/pulls/{index}/merge repository repoPullRequestIsMerged
// ---
// summary: Check if a pull request has been merged
// 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
2018-10-21 06:40:42 +03:00
// format: int64
2017-11-13 10:02:25 +03:00
// required: true
// responses:
// "204":
// description: pull request has been merged
// "404":
// description: pull request has not been merged
2019-12-20 20:07:12 +03:00
2022-06-13 12:37:59 +03:00
pr , err := issues_model . GetPullRequestByIndex ( ctx , ctx . Repo . Repository . ID , ctx . ParamsInt64 ( ":index" ) )
2016-12-02 14:10:39 +03:00
if err != nil {
2022-06-13 12:37:59 +03:00
if issues_model . IsErrPullRequestNotExist ( err ) {
2019-03-19 05:29:43 +03:00
ctx . NotFound ( )
2016-12-02 14:10:39 +03:00
} else {
2019-12-20 20:07:12 +03:00
ctx . Error ( http . StatusInternalServerError , "GetPullRequestByIndex" , err )
2016-12-02 14:10:39 +03:00
}
return
}
if pr . HasMerged {
2019-12-20 20:07:12 +03:00
ctx . Status ( http . StatusNoContent )
2016-12-02 14:10:39 +03:00
}
2019-03-19 05:29:43 +03:00
ctx . NotFound ( )
2016-12-02 14:10:39 +03:00
}
// MergePullRequest merges a PR given an index
2021-01-26 18:36:53 +03:00
func MergePullRequest ( ctx * context . APIContext ) {
2017-11-13 10:02:25 +03:00
// swagger:operation POST /repos/{owner}/{repo}/pulls/{index}/merge repository repoMergePullRequest
// ---
// summary: Merge 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 to merge
// type: integer
2018-10-21 06:40:42 +03:00
// format: int64
2017-11-13 10:02:25 +03:00
// required: true
2019-02-08 11:08:38 +03:00
// - name: body
// in: body
// schema:
// $ref: "#/definitions/MergePullRequestOption"
2017-11-13 10:02:25 +03:00
// responses:
// "200":
// "$ref": "#/responses/empty"
// "405":
// "$ref": "#/responses/empty"
2019-12-20 20:07:12 +03:00
// "409":
// "$ref": "#/responses/error"
2021-04-06 22:44:05 +03:00
form := web . GetForm ( ctx ) . ( * forms . MergePullRequestForm )
2022-05-03 22:46:28 +03:00
2022-06-13 12:37:59 +03:00
pr , err := issues_model . GetPullRequestByIndex ( ctx , ctx . Repo . Repository . ID , ctx . ParamsInt64 ( ":index" ) )
2016-12-02 14:10:39 +03:00
if err != nil {
2022-06-13 12:37:59 +03:00
if issues_model . IsErrPullRequestNotExist ( err ) {
2018-01-11 00:34:17 +03:00
ctx . NotFound ( "GetPullRequestByIndex" , err )
2016-12-02 14:10:39 +03:00
} else {
2019-12-20 20:07:12 +03:00
ctx . Error ( http . StatusInternalServerError , "GetPullRequestByIndex" , err )
2016-12-02 14:10:39 +03:00
}
return
}
2022-11-19 11:12:33 +03:00
if err := pr . LoadHeadRepo ( ctx ) ; err != nil {
2020-09-20 23:20:14 +03:00
ctx . Error ( http . StatusInternalServerError , "LoadHeadRepo" , err )
2016-12-02 14:10:39 +03:00
return
}
2022-11-19 11:12:33 +03:00
if err := pr . LoadIssue ( ctx ) ; err != nil {
2019-06-12 22:41:28 +03:00
ctx . Error ( http . StatusInternalServerError , "LoadIssue" , err )
return
}
2016-12-02 14:10:39 +03:00
pr . Issue . Repo = ctx . Repo . Repository
if ctx . IsSigned {
// Update issue-user.
2022-08-25 05:31:57 +03:00
if err = activities_model . SetIssueReadBy ( ctx , pr . Issue . ID , ctx . Doer . ID ) ; err != nil {
2019-12-20 20:07:12 +03:00
ctx . Error ( http . StatusInternalServerError , "ReadBy" , err )
2016-12-02 14:10:39 +03:00
return
}
}
2023-02-21 17:42:07 +03:00
manuallyMerged := repo_model . MergeStyle ( form . Do ) == repo_model . MergeStyleManuallyMerged
mergeCheckType := pull_service . MergeCheckTypeGeneral
if form . MergeWhenChecksSucceed {
mergeCheckType = pull_service . MergeCheckTypeAuto
}
if manuallyMerged {
mergeCheckType = pull_service . MergeCheckTypeManually
}
2019-09-18 08:39:45 +03:00
2022-05-03 22:46:28 +03:00
// start with merging by checking
2023-02-21 17:42:07 +03:00
if err := pull_service . CheckPullMergable ( ctx , ctx . Doer , & ctx . Repo . Permission , pr , mergeCheckType , form . ForceMerge ) ; err != nil {
2022-03-31 17:53:08 +03:00
if errors . Is ( err , pull_service . ErrIsClosed ) {
ctx . NotFound ( )
} else if errors . Is ( err , pull_service . ErrUserNotAllowedToMerge ) {
ctx . Error ( http . StatusMethodNotAllowed , "Merge" , "User not allowed to merge PR" )
} else if errors . Is ( err , pull_service . ErrHasMerged ) {
ctx . Error ( http . StatusMethodNotAllowed , "PR already merged" , "" )
} else if errors . Is ( err , pull_service . ErrIsWorkInProgress ) {
ctx . Error ( http . StatusMethodNotAllowed , "PR is a work in progress" , "Work in progress PRs cannot be merged" )
} else if errors . Is ( err , pull_service . ErrNotMergableState ) {
ctx . Error ( http . StatusMethodNotAllowed , "PR not in mergeable state" , "Please try again later" )
} else if models . IsErrDisallowedToMerge ( err ) {
ctx . Error ( http . StatusMethodNotAllowed , "PR is not ready to be merged" , err )
} else if asymkey_service . IsErrWontSign ( err ) {
ctx . Error ( http . StatusMethodNotAllowed , fmt . Sprintf ( "Protected branch %s requires signed commits but this merge would not be signed" , pr . BaseBranch ) , err )
} else {
ctx . InternalServerError ( err )
}
2020-08-20 00:35:06 +03:00
return
}
2021-03-04 06:41:23 +03:00
// handle manually-merged mark
2023-02-21 17:42:07 +03:00
if manuallyMerged {
2022-03-31 17:53:08 +03:00
if err := pull_service . MergedManually ( pr , ctx . Doer , ctx . Repo . GitRepo , form . MergeCommitID ) ; err != nil {
2021-03-04 06:41:23 +03:00
if models . IsErrInvalidMergeStyle ( err ) {
2021-12-10 04:27:50 +03:00
ctx . Error ( http . StatusMethodNotAllowed , "Invalid merge style" , fmt . Errorf ( "%s is not allowed an allowed merge style for this repository" , repo_model . MergeStyle ( form . Do ) ) )
2021-03-04 06:41:23 +03:00
return
}
if strings . Contains ( err . Error ( ) , "Wrong commit ID" ) {
ctx . JSON ( http . StatusConflict , err )
return
}
ctx . Error ( http . StatusInternalServerError , "Manually-Merged" , err )
return
}
ctx . Status ( http . StatusOK )
return
}
2022-05-08 15:32:45 +03:00
if len ( form . Do ) == 0 {
form . Do = string ( repo_model . MergeStyleMerge )
}
message := strings . TrimSpace ( form . MergeTitleField )
if len ( message ) == 0 {
2022-12-29 15:40:20 +03:00
message , _ , err = pull_service . GetDefaultMergeMessage ( ctx , ctx . Repo . GitRepo , pr , repo_model . MergeStyle ( form . Do ) )
2022-05-08 15:32:45 +03:00
if err != nil {
ctx . Error ( http . StatusInternalServerError , "GetDefaultMergeMessage" , err )
return
}
}
form . MergeMessageField = strings . TrimSpace ( form . MergeMessageField )
if len ( form . MergeMessageField ) > 0 {
message += "\n\n" + form . MergeMessageField
2020-08-20 00:35:06 +03:00
}
2022-05-07 20:05:52 +03:00
if form . MergeWhenChecksSucceed {
2022-05-08 15:32:45 +03:00
scheduled , err := automerge . ScheduleAutoMerge ( ctx , ctx . Doer , pr , repo_model . MergeStyle ( form . Do ) , message )
2022-05-07 20:05:52 +03:00
if err != nil {
if pull_model . IsErrAlreadyScheduledToAutoMerge ( err ) {
ctx . Error ( http . StatusConflict , "ScheduleAutoMerge" , err )
return
}
ctx . Error ( http . StatusInternalServerError , "ScheduleAutoMerge" , err )
return
} else if scheduled {
// nothing more to do ...
ctx . Status ( http . StatusCreated )
return
}
}
2022-11-03 18:49:00 +03:00
if err := pull_service . Merge ( ctx , pr , ctx . Doer , ctx . Repo . GitRepo , repo_model . MergeStyle ( form . Do ) , form . HeadCommitID , message , false ) ; err != nil {
2018-01-05 21:56:50 +03:00
if models . IsErrInvalidMergeStyle ( err ) {
2021-12-10 04:27:50 +03:00
ctx . Error ( http . StatusMethodNotAllowed , "Invalid merge style" , fmt . Errorf ( "%s is not allowed an allowed merge style for this repository" , repo_model . MergeStyle ( form . Do ) ) )
2019-11-10 11:42:51 +03:00
} else if models . IsErrMergeConflicts ( err ) {
conflictError := err . ( models . ErrMergeConflicts )
ctx . JSON ( http . StatusConflict , conflictError )
} else if models . IsErrRebaseConflicts ( err ) {
conflictError := err . ( models . ErrRebaseConflicts )
ctx . JSON ( http . StatusConflict , conflictError )
} else if models . IsErrMergeUnrelatedHistories ( err ) {
conflictError := err . ( models . ErrMergeUnrelatedHistories )
ctx . JSON ( http . StatusConflict , conflictError )
2020-03-28 07:13:18 +03:00
} else if git . IsErrPushOutOfDate ( err ) {
2019-12-20 20:07:12 +03:00
ctx . Error ( http . StatusConflict , "Merge" , "merge push out of date" )
2021-12-20 03:32:54 +03:00
} else if models . IsErrSHADoesNotMatch ( err ) {
ctx . Error ( http . StatusConflict , "Merge" , "head out of date" )
2020-03-28 07:13:18 +03:00
} else if git . IsErrPushRejected ( err ) {
errPushRej := err . ( * git . ErrPushRejected )
2020-02-22 16:08:48 +03:00
if len ( errPushRej . Message ) == 0 {
ctx . Error ( http . StatusConflict , "Merge" , "PushRejected without remote error message" )
2022-05-03 22:46:28 +03:00
} else {
ctx . Error ( http . StatusConflict , "Merge" , "PushRejected with remote message: " + errPushRej . Message )
2020-02-22 16:08:48 +03:00
}
2022-05-03 22:46:28 +03:00
} else {
ctx . Error ( http . StatusInternalServerError , "Merge" , err )
2018-01-05 21:56:50 +03:00
}
2016-12-02 14:10:39 +03:00
return
}
log . Trace ( "Pull request merged: %d" , pr . ID )
2021-07-13 02:26:25 +03:00
if form . DeleteBranchAfterMerge {
2022-01-03 22:45:58 +03:00
// Don't cleanup when there are other PR's that use this branch as head branch.
2022-06-13 12:37:59 +03:00
exist , err := issues_model . HasUnmergedPullRequestsByHeadInfo ( ctx , pr . HeadRepoID , pr . HeadBranch )
2022-01-03 22:45:58 +03:00
if err != nil {
ctx . ServerError ( "HasUnmergedPullRequestsByHeadInfo" , err )
return
}
if exist {
ctx . Status ( http . StatusOK )
return
}
2021-07-13 02:26:25 +03:00
var headRepo * git . Repository
if ctx . Repo != nil && ctx . Repo . Repository != nil && ctx . Repo . Repository . ID == pr . HeadRepoID && ctx . Repo . GitRepo != nil {
headRepo = ctx . Repo . GitRepo
} else {
2022-03-29 22:13:41 +03:00
headRepo , err = git . OpenRepository ( ctx , pr . HeadRepo . RepoPath ( ) )
2021-07-13 02:26:25 +03:00
if err != nil {
ctx . ServerError ( fmt . Sprintf ( "OpenRepository[%s]" , pr . HeadRepo . RepoPath ( ) ) , err )
return
}
defer headRepo . Close ( )
}
2023-03-01 01:17:51 +03:00
if err := repo_service . DeleteBranch ( ctx , ctx . Doer , pr . HeadRepo , headRepo , pr . HeadBranch ) ; err != nil {
2021-07-13 02:26:25 +03:00
switch {
case git . IsErrBranchNotExist ( err ) :
ctx . NotFound ( err )
case errors . Is ( err , repo_service . ErrBranchIsDefault ) :
ctx . Error ( http . StatusForbidden , "DefaultBranch" , fmt . Errorf ( "can not delete default branch" ) )
2023-01-16 11:00:22 +03:00
case errors . Is ( err , git_model . ErrBranchIsProtected ) :
2021-07-13 02:26:25 +03:00
ctx . Error ( http . StatusForbidden , "IsProtectedBranch" , fmt . Errorf ( "branch protected" ) )
default :
ctx . Error ( http . StatusInternalServerError , "DeleteBranch" , err )
}
return
}
2022-06-13 12:37:59 +03:00
if err := issues_model . AddDeletePRBranchComment ( ctx , ctx . Doer , pr . BaseRepo , pr . Issue . ID , pr . HeadBranch ) ; err != nil {
2021-07-13 02:26:25 +03:00
// Do not fail here as branch has already been deleted
log . Error ( "DeleteBranch: %v" , err )
}
}
2019-12-20 20:07:12 +03:00
ctx . Status ( http . StatusOK )
2016-12-02 14:10:39 +03:00
}
2021-12-10 04:27:50 +03:00
func parseCompareInfo ( ctx * context . APIContext , form api . CreatePullRequestOption ) ( * user_model . User , * repo_model . Repository , * git . Repository , * git . CompareInfo , string , string ) {
2016-12-02 14:10:39 +03:00
baseRepo := ctx . Repo . Repository
// Get compared branches information
// format: <base branch>...[<head repo>:]<head branch>
// base<-head: master...head:feature
// same repo: master...feature
// TODO: Validate form first?
baseBranch := form . Base
var (
2021-11-24 12:49:20 +03:00
headUser * user_model . User
2016-12-02 14:10:39 +03:00
headBranch string
isSameRepo bool
err error
)
// If there is no head repository, it means pull request between same repository.
headInfos := strings . Split ( form . Head , ":" )
if len ( headInfos ) == 1 {
isSameRepo = true
headUser = ctx . Repo . Owner
headBranch = headInfos [ 0 ]
} else if len ( headInfos ) == 2 {
2022-05-20 17:08:52 +03:00
headUser , err = user_model . GetUserByName ( ctx , headInfos [ 0 ] )
2016-12-02 14:10:39 +03:00
if err != nil {
2021-11-24 12:49:20 +03:00
if user_model . IsErrUserNotExist ( err ) {
2019-05-07 20:20:23 +03:00
ctx . NotFound ( "GetUserByName" )
2016-12-02 14:10:39 +03:00
} else {
2020-09-20 23:20:14 +03:00
ctx . Error ( http . StatusInternalServerError , "GetUserByName" , err )
2016-12-02 14:10:39 +03:00
}
return nil , nil , nil , nil , "" , ""
}
headBranch = headInfos [ 1 ]
} else {
2019-03-19 05:29:43 +03:00
ctx . NotFound ( )
2016-12-02 14:10:39 +03:00
return nil , nil , nil , nil , "" , ""
}
ctx . Repo . PullRequest . SameRepo = isSameRepo
log . Info ( "Base branch: %s" , baseBranch )
log . Info ( "Repo path: %s" , ctx . Repo . GitRepo . Path )
// Check if base branch is valid.
if ! ctx . Repo . GitRepo . IsBranchExist ( baseBranch ) {
2019-05-07 20:20:23 +03:00
ctx . NotFound ( "IsBranchExist" )
2016-12-02 14:10:39 +03:00
return nil , nil , nil , nil , "" , ""
}
// Check if current user has fork of repository or in the same repository.
2021-12-12 18:48:20 +03:00
headRepo := repo_model . GetForkedRepo ( headUser . ID , baseRepo . ID )
2021-11-22 18:21:55 +03:00
if headRepo == nil && ! isSameRepo {
2016-12-02 14:10:39 +03:00
log . Trace ( "parseCompareInfo[%d]: does not have fork or in same repository" , baseRepo . ID )
2021-11-22 18:21:55 +03:00
ctx . NotFound ( "GetForkedRepo" )
2016-12-02 14:10:39 +03:00
return nil , nil , nil , nil , "" , ""
}
var headGitRepo * git . Repository
if isSameRepo {
headRepo = ctx . Repo . Repository
headGitRepo = ctx . Repo . GitRepo
} else {
2022-03-29 22:13:41 +03:00
headGitRepo , err = git . OpenRepository ( ctx , repo_model . RepoPath ( headUser . Name , headRepo . Name ) )
2016-12-02 14:10:39 +03:00
if err != nil {
2019-12-20 20:07:12 +03:00
ctx . Error ( http . StatusInternalServerError , "OpenRepository" , err )
2016-12-02 14:10:39 +03:00
return nil , nil , nil , nil , "" , ""
}
}
2019-05-07 20:20:23 +03:00
// user should have permission to read baseRepo's codes and pulls, NOT headRepo's
2022-05-11 13:09:36 +03:00
permBase , err := access_model . GetUserRepoPermission ( ctx , baseRepo , ctx . Doer )
2018-11-28 14:26:14 +03:00
if err != nil {
2019-11-13 10:01:19 +03:00
headGitRepo . Close ( )
2020-09-20 23:20:14 +03:00
ctx . Error ( http . StatusInternalServerError , "GetUserRepoPermission" , err )
2018-11-28 14:26:14 +03:00
return nil , nil , nil , nil , "" , ""
}
2021-11-09 22:57:58 +03:00
if ! permBase . CanReadIssuesOrPulls ( true ) || ! permBase . CanRead ( unit . TypeCode ) {
2019-04-22 23:40:51 +03:00
if log . IsTrace ( ) {
2019-05-07 20:20:23 +03:00
log . Trace ( "Permission Denied: User %-v cannot create/read pull requests or cannot read code in Repo %-v\nUser in baseRepo has Permissions: %-+v" ,
2022-03-22 10:03:22 +03:00
ctx . Doer ,
2019-05-07 20:20:23 +03:00
baseRepo ,
permBase )
}
2019-11-13 10:01:19 +03:00
headGitRepo . Close ( )
2019-05-07 20:20:23 +03:00
ctx . NotFound ( "Can't read pulls or can't read UnitTypeCode" )
return nil , nil , nil , nil , "" , ""
}
// user should have permission to read headrepo's codes
2022-05-11 13:09:36 +03:00
permHead , err := access_model . GetUserRepoPermission ( ctx , headRepo , ctx . Doer )
2019-05-07 20:20:23 +03:00
if err != nil {
2019-11-13 10:01:19 +03:00
headGitRepo . Close ( )
2020-09-20 23:20:14 +03:00
ctx . Error ( http . StatusInternalServerError , "GetUserRepoPermission" , err )
2019-05-07 20:20:23 +03:00
return nil , nil , nil , nil , "" , ""
}
2021-11-09 22:57:58 +03:00
if ! permHead . CanRead ( unit . TypeCode ) {
2019-05-07 20:20:23 +03:00
if log . IsTrace ( ) {
log . Trace ( "Permission Denied: User: %-v cannot read code in Repo: %-v\nUser in headRepo has Permissions: %-+v" ,
2022-03-22 10:03:22 +03:00
ctx . Doer ,
2019-04-22 23:40:51 +03:00
headRepo ,
2019-05-07 20:20:23 +03:00
permHead )
2019-04-22 23:40:51 +03:00
}
2019-11-13 10:01:19 +03:00
headGitRepo . Close ( )
2019-05-07 20:20:23 +03:00
ctx . NotFound ( "Can't read headRepo UnitTypeCode" )
2016-12-02 14:10:39 +03:00
return nil , nil , nil , nil , "" , ""
}
// Check if head branch is valid.
if ! headGitRepo . IsBranchExist ( headBranch ) {
2019-11-13 10:01:19 +03:00
headGitRepo . Close ( )
2019-03-19 05:29:43 +03:00
ctx . NotFound ( )
2016-12-02 14:10:39 +03:00
return nil , nil , nil , nil , "" , ""
}
2022-01-18 10:45:43 +03:00
compareInfo , err := headGitRepo . GetCompareInfo ( repo_model . RepoPath ( baseRepo . Owner . Name , baseRepo . Name ) , baseBranch , headBranch , false , false )
2016-12-02 14:10:39 +03:00
if err != nil {
2019-11-13 10:01:19 +03:00
headGitRepo . Close ( )
2019-12-20 20:07:12 +03:00
ctx . Error ( http . StatusInternalServerError , "GetCompareInfo" , err )
2016-12-02 14:10:39 +03:00
return nil , nil , nil , nil , "" , ""
}
2019-06-07 23:29:29 +03:00
return headUser , headRepo , headGitRepo , compareInfo , baseBranch , headBranch
2016-12-02 14:10:39 +03:00
}
2020-08-04 23:55:22 +03:00
// UpdatePullRequest merge PR's baseBranch into headBranch
func UpdatePullRequest ( ctx * context . APIContext ) {
// swagger:operation POST /repos/{owner}/{repo}/pulls/{index}/update repository repoUpdatePullRequest
// ---
// summary: Merge PR's baseBranch into headBranch
// 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 to get
// type: integer
// format: int64
// required: true
2021-08-31 17:03:45 +03:00
// - name: style
// in: query
// description: how to update pull request
// type: string
// enum: [merge, rebase]
2020-08-04 23:55:22 +03:00
// responses:
// "200":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
// "409":
// "$ref": "#/responses/error"
// "422":
// "$ref": "#/responses/validationError"
2022-06-13 12:37:59 +03:00
pr , err := issues_model . GetPullRequestByIndex ( ctx , ctx . Repo . Repository . ID , ctx . ParamsInt64 ( ":index" ) )
2020-08-04 23:55:22 +03:00
if err != nil {
2022-06-13 12:37:59 +03:00
if issues_model . IsErrPullRequestNotExist ( err ) {
2020-08-04 23:55:22 +03:00
ctx . NotFound ( )
} else {
ctx . Error ( http . StatusInternalServerError , "GetPullRequestByIndex" , err )
}
return
}
if pr . HasMerged {
ctx . Error ( http . StatusUnprocessableEntity , "UpdatePullRequest" , err )
return
}
2022-11-19 11:12:33 +03:00
if err = pr . LoadIssue ( ctx ) ; err != nil {
2020-08-04 23:55:22 +03:00
ctx . Error ( http . StatusInternalServerError , "LoadIssue" , err )
return
}
if pr . Issue . IsClosed {
ctx . Error ( http . StatusUnprocessableEntity , "UpdatePullRequest" , err )
return
}
2022-11-19 11:12:33 +03:00
if err = pr . LoadBaseRepo ( ctx ) ; err != nil {
2020-08-04 23:55:22 +03:00
ctx . Error ( http . StatusInternalServerError , "LoadBaseRepo" , err )
return
}
2022-11-19 11:12:33 +03:00
if err = pr . LoadHeadRepo ( ctx ) ; err != nil {
2020-08-04 23:55:22 +03:00
ctx . Error ( http . StatusInternalServerError , "LoadHeadRepo" , err )
return
}
2021-08-31 17:03:45 +03:00
rebase := ctx . FormString ( "style" ) == "rebase"
2022-04-28 14:48:48 +03:00
allowedUpdateByMerge , allowedUpdateByRebase , err := pull_service . IsUserAllowedToUpdate ( ctx , pr , ctx . Doer )
2020-08-04 23:55:22 +03:00
if err != nil {
ctx . Error ( http . StatusInternalServerError , "IsUserAllowedToMerge" , err )
return
}
2021-08-31 17:03:45 +03:00
if ( ! allowedUpdateByMerge && ! rebase ) || ( rebase && ! allowedUpdateByRebase ) {
2020-08-04 23:55:22 +03:00
ctx . Status ( http . StatusForbidden )
return
}
// default merge commit message
message := fmt . Sprintf ( "Merge branch '%s' into %s" , pr . BaseBranch , pr . HeadBranch )
2022-03-22 10:03:22 +03:00
if err = pull_service . Update ( ctx , pr , ctx . Doer , message , rebase ) ; err != nil {
2020-08-04 23:55:22 +03:00
if models . IsErrMergeConflicts ( err ) {
ctx . Error ( http . StatusConflict , "Update" , "merge failed because of conflict" )
return
2021-09-05 12:30:40 +03:00
} else if models . IsErrRebaseConflicts ( err ) {
ctx . Error ( http . StatusConflict , "Update" , "rebase failed because of conflict" )
return
2020-08-04 23:55:22 +03:00
}
ctx . Error ( http . StatusInternalServerError , "pull_service.Update" , err )
return
}
ctx . Status ( http . StatusOK )
}
2021-07-02 15:19:57 +03:00
2022-05-07 20:05:52 +03:00
// MergePullRequest cancel an auto merge scheduled for a given PullRequest by index
func CancelScheduledAutoMerge ( ctx * context . APIContext ) {
// swagger:operation DELETE /repos/{owner}/{repo}/pulls/{index}/merge repository repoCancelScheduledAutoMerge
// ---
// summary: Cancel the scheduled auto merge for the given 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 to merge
// type: integer
// format: int64
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
pullIndex := ctx . ParamsInt64 ( ":index" )
2022-06-13 12:37:59 +03:00
pull , err := issues_model . GetPullRequestByIndex ( ctx , ctx . Repo . Repository . ID , pullIndex )
2022-05-07 20:05:52 +03:00
if err != nil {
2022-06-13 12:37:59 +03:00
if issues_model . IsErrPullRequestNotExist ( err ) {
2022-05-07 20:05:52 +03:00
ctx . NotFound ( )
return
}
ctx . InternalServerError ( err )
return
}
exist , autoMerge , err := pull_model . GetScheduledMergeByPullID ( ctx , pull . ID )
if err != nil {
ctx . InternalServerError ( err )
return
}
if ! exist {
ctx . NotFound ( )
return
}
if ctx . Doer . ID != autoMerge . DoerID {
2022-05-20 17:08:52 +03:00
allowed , err := access_model . IsUserRepoAdmin ( ctx , ctx . Repo . Repository , ctx . Doer )
2022-05-07 20:05:52 +03:00
if err != nil {
ctx . InternalServerError ( err )
return
}
if ! allowed {
ctx . Error ( http . StatusForbidden , "No permission to cancel" , "user has no permission to cancel the scheduled auto merge" )
return
}
}
2022-05-08 16:46:34 +03:00
if err := automerge . RemoveScheduledAutoMerge ( ctx , ctx . Doer , pull ) ; err != nil {
2022-05-07 20:05:52 +03:00
ctx . InternalServerError ( err )
} else {
ctx . Status ( http . StatusNoContent )
}
}
2021-07-02 15:19:57 +03:00
// GetPullRequestCommits gets all commits associated with a given PR
func GetPullRequestCommits ( ctx * context . APIContext ) {
// swagger:operation GET /repos/{owner}/{repo}/pulls/{index}/commits repository repoGetPullRequestCommits
// ---
// summary: Get commits 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 to get
// 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
// type: integer
// responses:
// "200":
// "$ref": "#/responses/CommitList"
// "404":
// "$ref": "#/responses/notFound"
2022-06-13 12:37:59 +03:00
pr , err := issues_model . GetPullRequestByIndex ( ctx , ctx . Repo . Repository . ID , ctx . ParamsInt64 ( ":index" ) )
2021-07-02 15:19:57 +03:00
if err != nil {
2022-06-13 12:37:59 +03:00
if issues_model . IsErrPullRequestNotExist ( err ) {
2021-07-02 15:19:57 +03:00
ctx . NotFound ( )
} else {
ctx . Error ( http . StatusInternalServerError , "GetPullRequestByIndex" , err )
}
return
}
2022-11-19 11:12:33 +03:00
if err := pr . LoadBaseRepo ( ctx ) ; err != nil {
2021-07-02 15:19:57 +03:00
ctx . InternalServerError ( err )
return
}
var prInfo * git . CompareInfo
2022-01-20 02:26:57 +03:00
baseGitRepo , closer , err := git . RepositoryFromContextOrOpen ( ctx , pr . BaseRepo . RepoPath ( ) )
2021-07-02 15:19:57 +03:00
if err != nil {
ctx . ServerError ( "OpenRepository" , err )
return
}
2022-01-20 02:26:57 +03:00
defer closer . Close ( )
2021-07-02 15:19:57 +03:00
if pr . HasMerged {
2022-01-18 10:45:43 +03:00
prInfo , err = baseGitRepo . GetCompareInfo ( pr . BaseRepo . RepoPath ( ) , pr . MergeBase , pr . GetGitRefName ( ) , false , false )
2021-07-02 15:19:57 +03:00
} else {
2022-01-18 10:45:43 +03:00
prInfo , err = baseGitRepo . GetCompareInfo ( pr . BaseRepo . RepoPath ( ) , pr . BaseBranch , pr . GetGitRefName ( ) , false , false )
2021-07-02 15:19:57 +03:00
}
if err != nil {
ctx . ServerError ( "GetCompareInfo" , err )
return
}
commits := prInfo . Commits
listOptions := utils . GetListOptions ( ctx )
2021-08-09 21:08:51 +03:00
totalNumberOfCommits := len ( commits )
2021-07-02 15:19:57 +03:00
totalNumberOfPages := int ( math . Ceil ( float64 ( totalNumberOfCommits ) / float64 ( listOptions . PageSize ) ) )
2021-11-24 12:49:20 +03:00
userCache := make ( map [ string ] * user_model . User )
2021-07-02 15:19:57 +03:00
start , end := listOptions . GetStartEnd ( )
if end > totalNumberOfCommits {
end = totalNumberOfCommits
}
2021-08-09 21:08:51 +03:00
apiCommits := make ( [ ] * api . Commit , 0 , end - start )
for i := start ; i < end ; i ++ {
2023-05-09 04:06:05 +03:00
apiCommit , err := convert . ToCommit ( ctx , ctx . Repo . Repository , baseGitRepo , commits [ i ] , userCache , convert . ToCommitOptions { Stat : true } )
2021-07-02 15:19:57 +03:00
if err != nil {
ctx . ServerError ( "toCommit" , err )
return
}
2021-08-09 21:08:51 +03:00
apiCommits = append ( apiCommits , apiCommit )
2021-07-02 15:19:57 +03:00
}
2021-08-12 15:43:08 +03:00
ctx . SetLinkHeader ( totalNumberOfCommits , listOptions . PageSize )
ctx . SetTotalCountHeader ( int64 ( totalNumberOfCommits ) )
2021-07-02 15:19:57 +03:00
2021-12-15 09:59:57 +03:00
ctx . RespHeader ( ) . Set ( "X-Page" , strconv . Itoa ( listOptions . Page ) )
ctx . RespHeader ( ) . Set ( "X-PerPage" , strconv . Itoa ( listOptions . PageSize ) )
ctx . RespHeader ( ) . Set ( "X-PageCount" , strconv . Itoa ( totalNumberOfPages ) )
ctx . RespHeader ( ) . Set ( "X-HasMore" , strconv . FormatBool ( listOptions . Page < totalNumberOfPages ) )
2021-08-12 15:43:08 +03:00
ctx . AppendAccessControlExposeHeaders ( "X-Page" , "X-PerPage" , "X-PageCount" , "X-HasMore" )
2021-07-02 15:19:57 +03:00
ctx . JSON ( http . StatusOK , & apiCommits )
}
2022-09-29 05:27:20 +03:00
// GetPullRequestFiles gets all changed files associated with a given PR
func GetPullRequestFiles ( ctx * context . APIContext ) {
// swagger:operation GET /repos/{owner}/{repo}/pulls/{index}/files repository repoGetPullRequestFiles
// ---
// summary: Get changed files 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 to get
// type: integer
// format: int64
// required: true
// - name: skip-to
// in: query
// description: skip to given file
// type: string
// - name: whitespace
// in: query
// description: whitespace behavior
// type: string
// enum: [ignore-all, ignore-change, ignore-eol, show-all]
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/ChangedFileList"
// "404":
// "$ref": "#/responses/notFound"
pr , err := issues_model . GetPullRequestByIndex ( ctx , ctx . Repo . Repository . ID , ctx . ParamsInt64 ( ":index" ) )
if err != nil {
if issues_model . IsErrPullRequestNotExist ( err ) {
ctx . NotFound ( )
} else {
ctx . Error ( http . StatusInternalServerError , "GetPullRequestByIndex" , err )
}
return
}
2022-11-19 11:12:33 +03:00
if err := pr . LoadBaseRepo ( ctx ) ; err != nil {
2022-09-29 05:27:20 +03:00
ctx . InternalServerError ( err )
return
}
2022-11-19 11:12:33 +03:00
if err := pr . LoadHeadRepo ( ctx ) ; err != nil {
2022-09-29 05:27:20 +03:00
ctx . InternalServerError ( err )
return
}
baseGitRepo := ctx . Repo . GitRepo
var prInfo * git . CompareInfo
if pr . HasMerged {
prInfo , err = baseGitRepo . GetCompareInfo ( pr . BaseRepo . RepoPath ( ) , pr . MergeBase , pr . GetGitRefName ( ) , true , false )
} else {
prInfo , err = baseGitRepo . GetCompareInfo ( pr . BaseRepo . RepoPath ( ) , pr . BaseBranch , pr . GetGitRefName ( ) , true , false )
}
if err != nil {
ctx . ServerError ( "GetCompareInfo" , err )
return
}
headCommitID , err := baseGitRepo . GetRefCommitID ( pr . GetGitRefName ( ) )
if err != nil {
ctx . ServerError ( "GetRefCommitID" , err )
return
}
startCommitID := prInfo . MergeBase
endCommitID := headCommitID
2023-02-20 17:22:34 +03:00
maxLines := setting . Git . MaxGitDiffLines
2022-09-29 05:27:20 +03:00
2023-02-20 17:22:34 +03:00
// FIXME: If there are too many files in the repo, may cause some unpredictable issues.
2022-09-29 05:27:20 +03:00
diff , err := gitdiff . GetDiff ( baseGitRepo ,
& gitdiff . DiffOptions {
BeforeCommitID : startCommitID ,
AfterCommitID : endCommitID ,
SkipTo : ctx . FormString ( "skip-to" ) ,
MaxLines : maxLines ,
MaxLineCharacters : setting . Git . MaxGitDiffLineCharacters ,
2023-02-20 17:22:34 +03:00
MaxFiles : - 1 , // GetDiff() will return all files
2022-09-29 05:27:20 +03:00
WhitespaceBehavior : gitdiff . GetWhitespaceFlag ( ctx . FormString ( "whitespace" ) ) ,
} )
if err != nil {
ctx . ServerError ( "GetDiff" , err )
return
}
listOptions := utils . GetListOptions ( ctx )
totalNumberOfFiles := diff . NumFiles
totalNumberOfPages := int ( math . Ceil ( float64 ( totalNumberOfFiles ) / float64 ( listOptions . PageSize ) ) )
start , end := listOptions . GetStartEnd ( )
if end > totalNumberOfFiles {
end = totalNumberOfFiles
}
2022-10-26 18:46:11 +03:00
lenFiles := end - start
if lenFiles < 0 {
lenFiles = 0
}
2023-02-20 17:22:34 +03:00
2022-10-26 18:46:11 +03:00
apiFiles := make ( [ ] * api . ChangedFile , 0 , lenFiles )
2022-09-29 05:27:20 +03:00
for i := start ; i < end ; i ++ {
apiFiles = append ( apiFiles , convert . ToChangedFile ( diff . Files [ i ] , pr . HeadRepo , endCommitID ) )
}
ctx . SetLinkHeader ( totalNumberOfFiles , listOptions . PageSize )
ctx . SetTotalCountHeader ( int64 ( totalNumberOfFiles ) )
ctx . RespHeader ( ) . Set ( "X-Page" , strconv . Itoa ( listOptions . Page ) )
ctx . RespHeader ( ) . Set ( "X-PerPage" , strconv . Itoa ( listOptions . PageSize ) )
ctx . RespHeader ( ) . Set ( "X-PageCount" , strconv . Itoa ( totalNumberOfPages ) )
ctx . RespHeader ( ) . Set ( "X-HasMore" , strconv . FormatBool ( listOptions . Page < totalNumberOfPages ) )
ctx . AppendAccessControlExposeHeaders ( "X-Page" , "X-PerPage" , "X-PageCount" , "X-HasMore" )
ctx . JSON ( http . StatusOK , & apiFiles )
}