2016-12-02 14:10:39 +03:00
// Copyright 2016 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package repo
import (
"fmt"
2018-07-18 00:23:58 +03:00
"net/http"
2016-12-02 14:10:39 +03:00
"strings"
"code.gitea.io/git"
"code.gitea.io/gitea/models"
2018-01-05 21:56:50 +03:00
"code.gitea.io/gitea/modules/auth"
2016-12-02 14:10:39 +03:00
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
2018-05-01 22:05:28 +03:00
"code.gitea.io/gitea/modules/util"
2016-12-02 14:10:39 +03:00
api "code.gitea.io/sdk/gitea"
)
// ListPullRequests returns a list of all PRs
func ListPullRequests ( ctx * context . APIContext , form api . ListPullRequestsOptions ) {
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
// responses:
// "200":
// "$ref": "#/responses/PullRequestList"
2016-12-02 14:10:39 +03:00
prs , maxResults , err := models . PullRequests ( ctx . Repo . Repository . ID , & models . PullRequestsOptions {
Page : ctx . QueryInt ( "page" ) ,
State : ctx . QueryTrim ( "state" ) ,
SortType : ctx . QueryTrim ( "sort" ) ,
Labels : ctx . QueryStrings ( "labels" ) ,
MilestoneID : ctx . QueryInt64 ( "milestone" ) ,
} )
if err != nil {
ctx . Error ( 500 , "PullRequests" , err )
return
}
apiPrs := make ( [ ] * api . PullRequest , len ( prs ) )
for i := range prs {
2017-11-04 21:10:01 +03:00
if err = prs [ i ] . LoadIssue ( ) ; err != nil {
ctx . Error ( 500 , "LoadIssue" , err )
return
}
if err = prs [ i ] . LoadAttributes ( ) ; err != nil {
ctx . Error ( 500 , "LoadAttributes" , err )
return
}
if err = prs [ i ] . GetBaseRepo ( ) ; err != nil {
ctx . Error ( 500 , "GetBaseRepo" , err )
return
}
if err = prs [ i ] . GetHeadRepo ( ) ; err != nil {
ctx . Error ( 500 , "GetHeadRepo" , err )
return
}
2016-12-02 14:10:39 +03:00
apiPrs [ i ] = prs [ i ] . APIFormat ( )
}
ctx . SetLinkHeader ( int ( maxResults ) , models . ItemsPerPage )
ctx . JSON ( 200 , & apiPrs )
}
// 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
// required: true
// responses:
// "200":
// "$ref": "#/responses/PullRequest"
2016-12-02 14:10:39 +03:00
pr , err := models . GetPullRequestByIndex ( ctx . Repo . Repository . ID , ctx . ParamsInt64 ( ":index" ) )
if err != nil {
if models . IsErrPullRequestNotExist ( err ) {
ctx . Status ( 404 )
} else {
ctx . Error ( 500 , "GetPullRequestByIndex" , err )
}
return
}
2017-11-04 21:10:01 +03:00
if err = pr . GetBaseRepo ( ) ; err != nil {
ctx . Error ( 500 , "GetBaseRepo" , err )
return
}
if err = pr . GetHeadRepo ( ) ; err != nil {
ctx . Error ( 500 , "GetHeadRepo" , err )
return
}
2016-12-02 14:10:39 +03:00
ctx . JSON ( 200 , pr . APIFormat ( ) )
}
// CreatePullRequest does what it says
func CreatePullRequest ( ctx * context . APIContext , form api . CreatePullRequestOption ) {
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"
2016-12-02 14:10:39 +03:00
var (
repo = ctx . Repo . Repository
labelIDs [ ] int64
assigneeID int64
milestoneID int64
)
// Get repo/branch information
headUser , headRepo , headGitRepo , prInfo , baseBranch , headBranch := parseCompareInfo ( ctx , form )
if ctx . Written ( ) {
return
}
// Check if another PR exists with the same targets
existingPr , err := models . GetUnmergedPullRequest ( headRepo . ID , ctx . Repo . Repository . ID , headBranch , baseBranch )
if err != nil {
if ! models . IsErrPullRequestNotExist ( err ) {
ctx . Error ( 500 , "GetUnmergedPullRequest" , err )
return
}
} else {
err = models . ErrPullRequestAlreadyExists {
ID : existingPr . ID ,
IssueID : existingPr . Index ,
HeadRepoID : existingPr . HeadRepoID ,
BaseRepoID : existingPr . BaseRepoID ,
HeadBranch : existingPr . HeadBranch ,
BaseBranch : existingPr . BaseBranch ,
}
ctx . Error ( 409 , "GetUnmergedPullRequest" , err )
return
}
if len ( form . Labels ) > 0 {
labels , err := models . GetLabelsInRepoByIDs ( ctx . Repo . Repository . ID , form . Labels )
if err != nil {
ctx . Error ( 500 , "GetLabelsInRepoByIDs" , err )
return
}
labelIDs = make ( [ ] int64 , len ( labels ) )
for i := range labels {
labelIDs [ i ] = labels [ i ] . ID
}
}
if form . Milestone > 0 {
milestone , err := models . GetMilestoneByRepoID ( ctx . Repo . Repository . ID , milestoneID )
if err != nil {
if models . IsErrMilestoneNotExist ( err ) {
ctx . Status ( 404 )
} else {
ctx . Error ( 500 , "GetMilestoneByRepoID" , err )
}
return
}
milestoneID = milestone . ID
}
patch , err := headGitRepo . GetPatch ( prInfo . MergeBase , headBranch )
if err != nil {
ctx . Error ( 500 , "GetPatch" , err )
return
}
2018-05-01 22:05:28 +03:00
var deadlineUnix util . TimeStamp
if form . Deadline != nil {
deadlineUnix = util . TimeStamp ( form . Deadline . Unix ( ) )
}
2016-12-02 14:10:39 +03:00
prIssue := & models . Issue {
2018-05-01 22:05:28 +03:00
RepoID : repo . ID ,
Index : repo . NextIssueIndex ( ) ,
Title : form . Title ,
PosterID : ctx . User . ID ,
Poster : ctx . User ,
MilestoneID : milestoneID ,
AssigneeID : assigneeID ,
IsPull : true ,
Content : form . Body ,
DeadlineUnix : deadlineUnix ,
2016-12-02 14:10:39 +03:00
}
pr := & models . PullRequest {
HeadRepoID : headRepo . ID ,
BaseRepoID : repo . ID ,
HeadUserName : headUser . Name ,
HeadBranch : headBranch ,
BaseBranch : baseBranch ,
HeadRepo : headRepo ,
BaseRepo : repo ,
MergeBase : prInfo . MergeBase ,
Type : models . PullRequestGitea ,
}
2018-05-09 19:29:04 +03:00
// Get all assignee IDs
assigneeIDs , err := models . MakeIDsFromAPIAssigneesToAdd ( form . Assignee , form . Assignees )
if err != nil {
if models . IsErrUserNotExist ( err ) {
ctx . Error ( 422 , "" , fmt . Sprintf ( "Assignee does not exist: [name: %s]" , err ) )
} else {
ctx . Error ( 500 , "AddAssigneeByName" , err )
}
return
}
if err := models . NewPullRequest ( repo , prIssue , labelIDs , [ ] string { } , pr , patch , assigneeIDs ) ; err != nil {
if models . IsErrUserDoesNotHaveAccessToRepo ( err ) {
ctx . Error ( 400 , "UserDoesNotHaveAccessToRepo" , err )
return
}
2016-12-02 14:10:39 +03:00
ctx . Error ( 500 , "NewPullRequest" , err )
return
} else if err := pr . PushToBaseRepo ( ) ; err != nil {
ctx . Error ( 500 , "PushToBaseRepo" , err )
return
}
log . Trace ( "Pull request created: %d/%d" , repo . ID , prIssue . ID )
ctx . JSON ( 201 , pr . APIFormat ( ) )
}
// EditPullRequest does what it says
func EditPullRequest ( ctx * context . APIContext , form api . EditPullRequestOption ) {
2017-11-13 10:02:25 +03:00
// swagger:operation PATCH /repos/{owner}/{repo}/pulls/{index} repository repoEditPullRequest
// ---
// summary: Update 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: index
// in: path
// description: index of the pull request to edit
// type: integer
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/EditPullRequestOption"
// responses:
// "201":
// "$ref": "#/responses/PullRequest"
2016-12-02 14:10:39 +03:00
pr , err := models . GetPullRequestByIndex ( ctx . Repo . Repository . ID , ctx . ParamsInt64 ( ":index" ) )
if err != nil {
if models . IsErrPullRequestNotExist ( err ) {
ctx . Status ( 404 )
} else {
ctx . Error ( 500 , "GetPullRequestByIndex" , err )
}
return
}
pr . LoadIssue ( )
issue := pr . Issue
if ! issue . IsPoster ( ctx . User . ID ) && ! ctx . Repo . IsWriter ( ) {
ctx . Status ( 403 )
return
}
if len ( form . Title ) > 0 {
issue . Title = form . Title
}
if len ( form . Body ) > 0 {
issue . Content = form . Body
}
2018-05-09 19:29:04 +03:00
// Update Deadline
2018-05-01 22:05:28 +03:00
var deadlineUnix util . TimeStamp
if form . Deadline != nil && ! form . Deadline . IsZero ( ) {
deadlineUnix = util . TimeStamp ( form . Deadline . Unix ( ) )
}
if err := models . UpdateIssueDeadline ( issue , deadlineUnix , ctx . User ) ; err != nil {
ctx . Error ( 500 , "UpdateIssueDeadline" , err )
return
}
2018-05-09 19:29:04 +03:00
// Add/delete assignees
2016-12-02 14:10:39 +03:00
2018-05-09 19:29:04 +03:00
// Deleting is done the Github way (quote from their api documentation):
// 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.
if ctx . Repo . IsWriter ( ) && ( form . Assignees != nil || len ( form . Assignee ) > 0 ) {
err = models . UpdateAPIAssignee ( issue , form . Assignee , form . Assignees , ctx . User )
if err != nil {
if models . IsErrUserNotExist ( err ) {
ctx . Error ( 422 , "" , fmt . Sprintf ( "Assignee does not exist: [name: %s]" , err ) )
} else {
ctx . Error ( 500 , "UpdateAPIAssignee" , err )
}
2016-12-02 14:10:39 +03:00
return
}
}
2018-05-09 19:29:04 +03:00
2016-12-02 14:10:39 +03:00
if ctx . Repo . IsWriter ( ) && form . Milestone != 0 &&
issue . MilestoneID != form . Milestone {
oldMilestoneID := issue . MilestoneID
issue . MilestoneID = form . Milestone
2017-02-01 05:36:08 +03:00
if err = models . ChangeMilestoneAssign ( issue , ctx . User , oldMilestoneID ) ; err != nil {
2016-12-02 14:10:39 +03:00
ctx . Error ( 500 , "ChangeMilestoneAssign" , err )
return
}
}
if err = models . UpdateIssue ( issue ) ; err != nil {
ctx . Error ( 500 , "UpdateIssue" , err )
return
}
if form . State != nil {
if err = issue . ChangeStatus ( ctx . User , ctx . Repo . Repository , api . StateClosed == api . StateType ( * form . State ) ) ; err != nil {
2018-07-18 00:23:58 +03:00
if models . IsErrDependenciesLeft ( err ) {
ctx . Error ( http . StatusPreconditionFailed , "DependenciesLeft" , "cannot close this pull request because it still has open dependencies" )
return
}
2016-12-02 14:10:39 +03:00
ctx . Error ( 500 , "ChangeStatus" , err )
return
}
}
// Refetch from database
pr , err = models . GetPullRequestByIndex ( ctx . Repo . Repository . ID , pr . Index )
if err != nil {
if models . IsErrPullRequestNotExist ( err ) {
ctx . Status ( 404 )
} else {
ctx . Error ( 500 , "GetPullRequestByIndex" , err )
}
return
}
2017-11-13 10:02:25 +03:00
// TODO this should be 200, not 201
2016-12-02 14:10:39 +03:00
ctx . JSON ( 201 , pr . APIFormat ( ) )
}
// 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
// required: true
// responses:
// "204":
// description: pull request has been merged
// schema:
// "$ref": "#/responses/empty"
// "404":
// description: pull request has not been merged
// schema:
// "$ref": "#/responses/empty"
2016-12-02 14:10:39 +03:00
pr , err := models . GetPullRequestByIndex ( ctx . Repo . Repository . ID , ctx . ParamsInt64 ( ":index" ) )
if err != nil {
if models . IsErrPullRequestNotExist ( err ) {
ctx . Status ( 404 )
} else {
ctx . Error ( 500 , "GetPullRequestByIndex" , err )
}
return
}
if pr . HasMerged {
ctx . Status ( 204 )
}
ctx . Status ( 404 )
}
// MergePullRequest merges a PR given an index
2018-01-05 21:56:50 +03:00
func MergePullRequest ( ctx * context . APIContext , form auth . MergePullRequestForm ) {
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
// required: true
// responses:
// "200":
// "$ref": "#/responses/empty"
// "405":
// "$ref": "#/responses/empty"
2016-12-02 14:10:39 +03:00
pr , err := models . GetPullRequestByIndex ( ctx . Repo . Repository . ID , ctx . ParamsInt64 ( ":index" ) )
if err != nil {
if models . IsErrPullRequestNotExist ( err ) {
2018-01-11 00:34:17 +03:00
ctx . NotFound ( "GetPullRequestByIndex" , err )
2016-12-02 14:10:39 +03:00
} else {
ctx . Error ( 500 , "GetPullRequestByIndex" , err )
}
return
}
if err = pr . GetHeadRepo ( ) ; err != nil {
2018-01-11 00:34:17 +03:00
ctx . ServerError ( "GetHeadRepo" , err )
2016-12-02 14:10:39 +03:00
return
}
pr . LoadIssue ( )
pr . Issue . Repo = ctx . Repo . Repository
if ctx . IsSigned {
// Update issue-user.
if err = pr . Issue . ReadBy ( ctx . User . ID ) ; err != nil {
ctx . Error ( 500 , "ReadBy" , err )
return
}
}
if pr . Issue . IsClosed {
ctx . Status ( 404 )
return
}
if ! pr . CanAutoMerge ( ) || pr . HasMerged {
ctx . Status ( 405 )
return
}
2018-01-05 21:56:50 +03:00
if len ( form . Do ) == 0 {
form . Do = string ( models . MergeStyleMerge )
}
message := strings . TrimSpace ( form . MergeTitleField )
if len ( message ) == 0 {
if models . MergeStyle ( form . Do ) == models . MergeStyleMerge {
message = pr . GetDefaultMergeMessage ( )
}
if models . MergeStyle ( form . Do ) == models . MergeStyleSquash {
message = pr . GetDefaultSquashMessage ( )
}
}
form . MergeMessageField = strings . TrimSpace ( form . MergeMessageField )
if len ( form . MergeMessageField ) > 0 {
message += "\n\n" + form . MergeMessageField
}
if err := pr . Merge ( ctx . User , ctx . Repo . GitRepo , models . MergeStyle ( form . Do ) , message ) ; err != nil {
if models . IsErrInvalidMergeStyle ( err ) {
ctx . Status ( 405 )
return
}
2016-12-02 14:10:39 +03:00
ctx . Error ( 500 , "Merge" , err )
return
}
log . Trace ( "Pull request merged: %d" , pr . ID )
ctx . Status ( 200 )
}
func parseCompareInfo ( ctx * context . APIContext , form api . CreatePullRequestOption ) ( * models . User , * models . Repository , * git . Repository , * git . PullRequestInfo , string , string ) {
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 (
headUser * models . User
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 {
headUser , err = models . GetUserByName ( headInfos [ 0 ] )
if err != nil {
if models . IsErrUserNotExist ( err ) {
2018-01-11 00:34:17 +03:00
ctx . NotFound ( "GetUserByName" , nil )
2016-12-02 14:10:39 +03:00
} else {
2018-01-11 00:34:17 +03:00
ctx . ServerError ( "GetUserByName" , err )
2016-12-02 14:10:39 +03:00
}
return nil , nil , nil , nil , "" , ""
}
headBranch = headInfos [ 1 ]
} else {
ctx . Status ( 404 )
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 ) {
ctx . Status ( 404 )
return nil , nil , nil , nil , "" , ""
}
// Check if current user has fork of repository or in the same repository.
headRepo , has := models . HasForkedRepo ( headUser . ID , baseRepo . ID )
if ! has && ! isSameRepo {
log . Trace ( "parseCompareInfo[%d]: does not have fork or in same repository" , baseRepo . ID )
ctx . Status ( 404 )
return nil , nil , nil , nil , "" , ""
}
var headGitRepo * git . Repository
if isSameRepo {
headRepo = ctx . Repo . Repository
headGitRepo = ctx . Repo . GitRepo
} else {
headGitRepo , err = git . OpenRepository ( models . RepoPath ( headUser . Name , headRepo . Name ) )
if err != nil {
ctx . Error ( 500 , "OpenRepository" , err )
return nil , nil , nil , nil , "" , ""
}
}
if ! ctx . User . IsWriterOfRepo ( headRepo ) && ! ctx . User . IsAdmin {
log . Trace ( "ParseCompareInfo[%d]: does not have write access or site admin" , baseRepo . ID )
ctx . Status ( 404 )
return nil , nil , nil , nil , "" , ""
}
// Check if head branch is valid.
if ! headGitRepo . IsBranchExist ( headBranch ) {
ctx . Status ( 404 )
return nil , nil , nil , nil , "" , ""
}
prInfo , err := headGitRepo . GetPullRequestInfo ( models . RepoPath ( baseRepo . Owner . Name , baseRepo . Name ) , baseBranch , headBranch )
if err != nil {
ctx . Error ( 500 , "GetPullRequestInfo" , err )
return nil , nil , nil , nil , "" , ""
}
return headUser , headRepo , headGitRepo , prInfo , baseBranch , headBranch
}