2016-12-02 16:40:39 +05:30
// Copyright 2016 The Gitea Authors. All rights reserved.
2022-11-27 23:50:29 +05:30
// SPDX-License-Identifier: MIT
2016-12-02 16:40:39 +05:30
package repo
import (
2021-07-13 04:56:25 +05:30
"errors"
2016-12-02 16:40:39 +05:30
"fmt"
2021-07-02 17:49:57 +05:30
"math"
2018-07-18 02:53:58 +05:30
"net/http"
2021-07-02 17:49:57 +05:30
"strconv"
2016-12-02 16:40:39 +05:30
"strings"
2019-11-03 20:16:32 +05:30
"time"
2016-12-02 16:40:39 +05:30
"code.gitea.io/gitea/models"
2022-08-25 08:01:57 +05:30
activities_model "code.gitea.io/gitea/models/activities"
2023-01-16 13:30:22 +05:30
git_model "code.gitea.io/gitea/models/git"
2022-04-08 14:41:15 +05:30
issues_model "code.gitea.io/gitea/models/issues"
2022-05-11 15:39:36 +05:30
access_model "code.gitea.io/gitea/models/perm/access"
2022-05-07 22:35:52 +05:30
pull_model "code.gitea.io/gitea/models/pull"
2021-12-10 06:57:50 +05:30
repo_model "code.gitea.io/gitea/models/repo"
2021-11-10 01:27:58 +05:30
"code.gitea.io/gitea/models/unit"
2021-11-24 15:19:20 +05:30
user_model "code.gitea.io/gitea/models/user"
2016-12-02 16:40:39 +05:30
"code.gitea.io/gitea/modules/context"
2019-03-27 15:03:00 +05:30
"code.gitea.io/gitea/modules/git"
2016-12-02 16:40:39 +05:30
"code.gitea.io/gitea/modules/log"
2020-05-17 02:35:19 +05:30
"code.gitea.io/gitea/modules/notification"
2022-09-29 07:57:20 +05:30
"code.gitea.io/gitea/modules/setting"
2019-05-11 15:51:34 +05:30
api "code.gitea.io/gitea/modules/structs"
2019-08-15 20:16:21 +05:30
"code.gitea.io/gitea/modules/timeutil"
2021-01-26 21:06:53 +05:30
"code.gitea.io/gitea/modules/web"
2020-01-25 00:30:29 +05:30
"code.gitea.io/gitea/routers/api/v1/utils"
2021-12-10 13:44:24 +05:30
asymkey_service "code.gitea.io/gitea/services/asymkey"
2022-05-07 22:35:52 +05:30
"code.gitea.io/gitea/services/automerge"
2022-12-29 08:27:15 +05:30
"code.gitea.io/gitea/services/convert"
2021-04-07 01:14:05 +05:30
"code.gitea.io/gitea/services/forms"
2022-09-29 07:57:20 +05:30
"code.gitea.io/gitea/services/gitdiff"
2019-10-25 20:16:37 +05:30
issue_service "code.gitea.io/gitea/services/issue"
2019-09-27 05:52:36 +05:30
pull_service "code.gitea.io/gitea/services/pull"
2021-07-13 04:56:25 +05:30
repo_service "code.gitea.io/gitea/services/repository"
2016-12-02 16:40:39 +05:30
)
// ListPullRequests returns a list of all PRs
2021-01-26 21:06:53 +05:30
func ListPullRequests ( ctx * context . APIContext ) {
2017-11-13 12:32:25 +05:30
// 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 09:10:42 +05:30
// - 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-25 00:30:29 +05:30
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
2020-06-09 10:27:38 +05:30
// description: page size of results
2020-01-25 00:30:29 +05:30
// type: integer
2017-11-13 12:32:25 +05:30
// responses:
// "200":
// "$ref": "#/responses/PullRequestList"
2019-12-20 22:37:12 +05:30
2020-01-25 00:30:29 +05:30
listOptions := utils . GetListOptions ( ctx )
2022-06-13 15:07:59 +05:30
prs , maxResults , err := issues_model . PullRequests ( ctx . Repo . Repository . ID , & issues_model . PullRequestsOptions {
2020-01-25 00:30:29 +05:30
ListOptions : listOptions ,
2021-07-29 07:12:15 +05:30
State : ctx . FormTrim ( "state" ) ,
SortType : ctx . FormTrim ( "sort" ) ,
Labels : ctx . FormStrings ( "labels" ) ,
MilestoneID : ctx . FormInt64 ( "milestone" ) ,
2016-12-02 16:40:39 +05:30
} )
if err != nil {
2019-12-20 22:37:12 +05:30
ctx . Error ( http . StatusInternalServerError , "PullRequests" , err )
2016-12-02 16:40:39 +05:30
return
}
apiPrs := make ( [ ] * api . PullRequest , len ( prs ) )
for i := range prs {
2022-11-19 13:42:33 +05:30
if err = prs [ i ] . LoadIssue ( ctx ) ; err != nil {
2019-12-20 22:37:12 +05:30
ctx . Error ( http . StatusInternalServerError , "LoadIssue" , err )
2017-11-04 23:40:01 +05:30
return
}
2022-11-19 13:42:33 +05:30
if err = prs [ i ] . LoadAttributes ( ctx ) ; err != nil {
2019-12-20 22:37:12 +05:30
ctx . Error ( http . StatusInternalServerError , "LoadAttributes" , err )
2017-11-04 23:40:01 +05:30
return
}
2022-11-19 13:42:33 +05:30
if err = prs [ i ] . LoadBaseRepo ( ctx ) ; err != nil {
2020-03-03 04:01:55 +05:30
ctx . Error ( http . StatusInternalServerError , "LoadBaseRepo" , err )
2017-11-04 23:40:01 +05:30
return
}
2022-11-19 13:42:33 +05:30
if err = prs [ i ] . LoadHeadRepo ( ctx ) ; err != nil {
2020-03-03 04:01:55 +05:30
ctx . Error ( http . StatusInternalServerError , "LoadHeadRepo" , err )
2017-11-04 23:40:01 +05:30
return
}
2022-03-22 12:33:22 +05:30
apiPrs [ i ] = convert . ToAPIPullRequest ( ctx , prs [ i ] , ctx . Doer )
2016-12-02 16:40:39 +05:30
}
2020-01-25 00:30:29 +05:30
ctx . SetLinkHeader ( int ( maxResults ) , listOptions . PageSize )
2021-08-12 18:13:08 +05:30
ctx . SetTotalCountHeader ( maxResults )
2019-12-20 22:37:12 +05:30
ctx . JSON ( http . StatusOK , & apiPrs )
2016-12-02 16:40:39 +05:30
}
// GetPullRequest returns a single PR based on index
func GetPullRequest ( ctx * context . APIContext ) {
2017-11-13 12:32:25 +05:30
// 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 09:10:42 +05:30
// format: int64
2017-11-13 12:32:25 +05:30
// required: true
// responses:
// "200":
// "$ref": "#/responses/PullRequest"
2020-01-09 17:26:32 +05:30
// "404":
// "$ref": "#/responses/notFound"
2019-12-20 22:37:12 +05:30
2022-06-13 15:07:59 +05:30
pr , err := issues_model . GetPullRequestByIndex ( ctx , ctx . Repo . Repository . ID , ctx . ParamsInt64 ( ":index" ) )
2016-12-02 16:40:39 +05:30
if err != nil {
2022-06-13 15:07:59 +05:30
if issues_model . IsErrPullRequestNotExist ( err ) {
2019-03-19 07:59:43 +05:30
ctx . NotFound ( )
2016-12-02 16:40:39 +05:30
} else {
2019-12-20 22:37:12 +05:30
ctx . Error ( http . StatusInternalServerError , "GetPullRequestByIndex" , err )
2016-12-02 16:40:39 +05:30
}
return
}
2022-11-19 13:42:33 +05:30
if err = pr . LoadBaseRepo ( ctx ) ; err != nil {
2020-03-03 04:01:55 +05:30
ctx . Error ( http . StatusInternalServerError , "LoadBaseRepo" , err )
2017-11-04 23:40:01 +05:30
return
}
2022-11-19 13:42:33 +05:30
if err = pr . LoadHeadRepo ( ctx ) ; err != nil {
2020-03-03 04:01:55 +05:30
ctx . Error ( http . StatusInternalServerError , "LoadHeadRepo" , err )
2017-11-04 23:40:01 +05:30
return
}
2022-03-22 12:33:22 +05:30
ctx . JSON ( http . StatusOK , convert . ToAPIPullRequest ( ctx , pr , ctx . Doer ) )
2016-12-02 16:40:39 +05:30
}
2021-09-22 04:34:53 +05:30
// 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 16:33:12 +05:30
// ---
2021-09-22 04:34:53 +05:30
// summary: Get a pull request diff or patch
2020-06-05 16:33:12 +05:30
// 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 04:34:53 +05:30
// - name: diffType
2020-06-05 16:33:12 +05:30
// in: path
2021-09-22 04:34:53 +05:30
// description: whether the output is diff or patch
2020-06-05 16:33:12 +05:30
// type: string
2021-09-22 04:34:53 +05:30
// enum: [diff, patch]
2020-06-05 16:33:12 +05:30
// required: true
2021-09-28 02:39:49 +05:30
// - 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 16:33:12 +05:30
// responses:
// "200":
// "$ref": "#/responses/string"
// "404":
// "$ref": "#/responses/notFound"
2022-06-13 15:07:59 +05:30
pr , err := issues_model . GetPullRequestByIndex ( ctx , ctx . Repo . Repository . ID , ctx . ParamsInt64 ( ":index" ) )
2020-06-05 16:33:12 +05:30
if err != nil {
2022-06-13 15:07:59 +05:30
if issues_model . IsErrPullRequestNotExist ( err ) {
2020-06-05 16:33:12 +05:30
ctx . NotFound ( )
} else {
ctx . InternalServerError ( err )
}
return
}
2021-09-22 04:34:53 +05:30
var patch bool
if ctx . Params ( ":diffType" ) == "diff" {
patch = false
} else {
patch = true
}
2020-06-05 16:33:12 +05:30
2021-09-28 02:39:49 +05:30
binary := ctx . FormBool ( "binary" )
2022-01-20 04:56:57 +05:30
if err := pull_service . DownloadDiffOrPatch ( ctx , pr , ctx , patch , binary ) ; err != nil {
2020-06-05 16:33:12 +05:30
ctx . InternalServerError ( err )
return
}
}
2016-12-02 16:40:39 +05:30
// CreatePullRequest does what it says
2021-01-26 21:06:53 +05:30
func CreatePullRequest ( ctx * context . APIContext ) {
2017-11-13 12:32:25 +05:30
// 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 22:37:12 +05:30
// "409":
// "$ref": "#/responses/error"
// "422":
// "$ref": "#/responses/validationError"
2021-01-26 21:06:53 +05:30
form := * web . GetForm ( ctx ) . ( * api . CreatePullRequestOption )
2020-10-26 14:35:27 +05:30
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 16:40:39 +05:30
var (
repo = ctx . Repo . Repository
labelIDs [ ] int64
milestoneID int64
)
// Get repo/branch information
2019-10-18 16:43:31 +05:30
_ , headRepo , headGitRepo , compareInfo , baseBranch , headBranch := parseCompareInfo ( ctx , form )
2016-12-02 16:40:39 +05:30
if ctx . Written ( ) {
return
}
2019-11-13 12:31:19 +05:30
defer headGitRepo . Close ( )
2016-12-02 16:40:39 +05:30
// Check if another PR exists with the same targets
2022-11-19 13:42:33 +05:30
existingPr , err := issues_model . GetUnmergedPullRequest ( ctx , headRepo . ID , ctx . Repo . Repository . ID , headBranch , baseBranch , issues_model . PullRequestFlowGithub )
2016-12-02 16:40:39 +05:30
if err != nil {
2022-06-13 15:07:59 +05:30
if ! issues_model . IsErrPullRequestNotExist ( err ) {
2019-12-20 22:37:12 +05:30
ctx . Error ( http . StatusInternalServerError , "GetUnmergedPullRequest" , err )
2016-12-02 16:40:39 +05:30
return
}
} else {
2022-06-13 15:07:59 +05:30
err = issues_model . ErrPullRequestAlreadyExists {
2016-12-02 16:40:39 +05:30
ID : existingPr . ID ,
IssueID : existingPr . Index ,
HeadRepoID : existingPr . HeadRepoID ,
BaseRepoID : existingPr . BaseRepoID ,
HeadBranch : existingPr . HeadBranch ,
BaseBranch : existingPr . BaseBranch ,
}
2019-12-20 22:37:12 +05:30
ctx . Error ( http . StatusConflict , "GetUnmergedPullRequest" , err )
2016-12-02 16:40:39 +05:30
return
}
if len ( form . Labels ) > 0 {
2022-11-19 13:42:33 +05:30
labels , err := issues_model . GetLabelsInRepoByIDs ( ctx , ctx . Repo . Repository . ID , form . Labels )
2016-12-02 16:40:39 +05:30
if err != nil {
2019-12-20 22:37:12 +05:30
ctx . Error ( http . StatusInternalServerError , "GetLabelsInRepoByIDs" , err )
2016-12-02 16:40:39 +05:30
return
}
2020-04-20 18:40:45 +05:30
labelIDs = make ( [ ] int64 , len ( form . Labels ) )
orgLabelIDs := make ( [ ] int64 , len ( form . Labels ) )
2016-12-02 16:40:39 +05:30
for i := range labels {
labelIDs [ i ] = labels [ i ] . ID
}
2020-04-20 18:40:45 +05:30
if ctx . Repo . Owner . IsOrganization ( ) {
2022-11-19 13:42:33 +05:30
orgLabels , err := issues_model . GetLabelsInOrgByIDs ( ctx , ctx . Repo . Owner . ID , form . Labels )
2020-04-20 18:40:45 +05:30
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 16:40:39 +05:30
}
if form . Milestone > 0 {
2022-04-08 14:41:15 +05:30
milestone , err := issues_model . GetMilestoneByRepoID ( ctx , ctx . Repo . Repository . ID , form . Milestone )
2016-12-02 16:40:39 +05:30
if err != nil {
2022-04-08 14:41:15 +05:30
if issues_model . IsErrMilestoneNotExist ( err ) {
2019-03-19 07:59:43 +05:30
ctx . NotFound ( )
2016-12-02 16:40:39 +05:30
} else {
2019-12-20 22:37:12 +05:30
ctx . Error ( http . StatusInternalServerError , "GetMilestoneByRepoID" , err )
2016-12-02 16:40:39 +05:30
}
return
}
milestoneID = milestone . ID
}
2019-08-15 20:16:21 +05:30
var deadlineUnix timeutil . TimeStamp
2018-05-02 00:35:28 +05:30
if form . Deadline != nil {
2019-08-15 20:16:21 +05:30
deadlineUnix = timeutil . TimeStamp ( form . Deadline . Unix ( ) )
2018-05-02 00:35:28 +05:30
}
2022-06-13 15:07:59 +05:30
prIssue := & issues_model . Issue {
2018-05-02 00:35:28 +05:30
RepoID : repo . ID ,
Title : form . Title ,
2022-03-22 12:33:22 +05:30
PosterID : ctx . Doer . ID ,
Poster : ctx . Doer ,
2018-05-02 00:35:28 +05:30
MilestoneID : milestoneID ,
IsPull : true ,
Content : form . Body ,
DeadlineUnix : deadlineUnix ,
2016-12-02 16:40:39 +05:30
}
2022-06-13 15:07:59 +05:30
pr := & issues_model . PullRequest {
2019-10-18 16:43:31 +05:30
HeadRepoID : headRepo . ID ,
BaseRepoID : repo . ID ,
HeadBranch : headBranch ,
BaseBranch : baseBranch ,
HeadRepo : headRepo ,
BaseRepo : repo ,
MergeBase : compareInfo . MergeBase ,
2022-06-13 15:07:59 +05:30
Type : issues_model . PullRequestGitea ,
2016-12-02 16:40:39 +05:30
}
2018-05-09 21:59:04 +05:30
// Get all assignee IDs
2022-11-19 13:42:33 +05:30
assigneeIDs , err := issues_model . MakeIDsFromAPIAssigneesToAdd ( ctx , form . Assignee , form . Assignees )
2018-05-09 21:59:04 +05:30
if err != nil {
2021-11-24 15:19:20 +05:30
if user_model . IsErrUserNotExist ( err ) {
2019-12-20 22:37:12 +05:30
ctx . Error ( http . StatusUnprocessableEntity , "" , fmt . Sprintf ( "Assignee does not exist: [name: %s]" , err ) )
2018-05-09 21:59:04 +05:30
} else {
2019-12-20 22:37:12 +05:30
ctx . Error ( http . StatusInternalServerError , "AddAssigneeByName" , err )
2018-05-09 21:59:04 +05:30
}
return
}
2019-10-25 20:16:37 +05:30
// Check if the passed assignees is assignable
for _ , aID := range assigneeIDs {
2022-12-03 08:18:26 +05:30
assignee , err := user_model . GetUserByID ( ctx , aID )
2019-10-25 20:16:37 +05:30
if err != nil {
2019-12-20 22:37:12 +05:30
ctx . Error ( http . StatusInternalServerError , "GetUserByID" , err )
2019-10-25 20:16:37 +05:30
return
}
2018-05-09 21:59:04 +05:30
2022-05-11 15:39:36 +05:30
valid , err := access_model . CanBeAssigned ( ctx , assignee , repo , true )
2019-10-25 20:16:37 +05:30
if err != nil {
2019-12-20 22:37:12 +05:30
ctx . Error ( http . StatusInternalServerError , "canBeAssigned" , err )
2019-10-25 20:16:37 +05:30
return
}
if ! valid {
2022-06-13 15:07:59 +05:30
ctx . Error ( http . StatusUnprocessableEntity , "canBeAssigned" , repo_model . ErrUserDoesNotHaveAccessToRepo { UserID : aID , RepoName : repo . Name } )
2019-10-25 20:16:37 +05:30
return
}
}
2022-01-20 04:56:57 +05:30
if err := pull_service . NewPullRequest ( ctx , repo , prIssue , labelIDs , [ ] string { } , pr , assigneeIDs ) ; err != nil {
2022-06-13 15:07:59 +05:30
if repo_model . IsErrUserDoesNotHaveAccessToRepo ( err ) {
2019-12-20 22:37:12 +05:30
ctx . Error ( http . StatusBadRequest , "UserDoesNotHaveAccessToRepo" , err )
2018-05-09 21:59:04 +05:30
return
}
2019-12-20 22:37:12 +05:30
ctx . Error ( http . StatusInternalServerError , "NewPullRequest" , err )
2016-12-02 16:40:39 +05:30
return
}
log . Trace ( "Pull request created: %d/%d" , repo . ID , prIssue . ID )
2022-03-22 12:33:22 +05:30
ctx . JSON ( http . StatusCreated , convert . ToAPIPullRequest ( ctx , pr , ctx . Doer ) )
2016-12-02 16:40:39 +05:30
}
// EditPullRequest does what it says
2021-01-26 21:06:53 +05:30
func EditPullRequest ( ctx * context . APIContext ) {
2017-11-13 12:32:25 +05:30
// swagger:operation PATCH /repos/{owner}/{repo}/pulls/{index} repository repoEditPullRequest
// ---
2019-11-03 20:16:32 +05:30
// summary: Update a pull request. If using deadline only the date will be taken into account, and time of day ignored.
2017-11-13 12:32:25 +05:30
// 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 09:10:42 +05:30
// format: int64
2017-11-13 12:32:25 +05:30
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/EditPullRequestOption"
// responses:
// "201":
// "$ref": "#/responses/PullRequest"
2019-12-20 22:37:12 +05:30
// "403":
// "$ref": "#/responses/forbidden"
2020-06-08 00:43:40 +05:30
// "409":
// "$ref": "#/responses/error"
2019-12-20 22:37:12 +05:30
// "412":
// "$ref": "#/responses/error"
// "422":
// "$ref": "#/responses/validationError"
2021-01-26 21:06:53 +05:30
form := web . GetForm ( ctx ) . ( * api . EditPullRequestOption )
2022-06-13 15:07:59 +05:30
pr , err := issues_model . GetPullRequestByIndex ( ctx , ctx . Repo . Repository . ID , ctx . ParamsInt64 ( ":index" ) )
2016-12-02 16:40:39 +05:30
if err != nil {
2022-06-13 15:07:59 +05:30
if issues_model . IsErrPullRequestNotExist ( err ) {
2019-03-19 07:59:43 +05:30
ctx . NotFound ( )
2016-12-02 16:40:39 +05:30
} else {
2019-12-20 22:37:12 +05:30
ctx . Error ( http . StatusInternalServerError , "GetPullRequestByIndex" , err )
2016-12-02 16:40:39 +05:30
}
return
}
2022-11-19 13:42:33 +05:30
err = pr . LoadIssue ( ctx )
2019-06-13 01:11:28 +05:30
if err != nil {
ctx . Error ( http . StatusInternalServerError , "LoadIssue" , err )
return
}
2016-12-02 16:40:39 +05:30
issue := pr . Issue
2018-12-13 21:25:43 +05:30
issue . Repo = ctx . Repo . Repository
2016-12-02 16:40:39 +05:30
2022-12-22 04:15:44 +05:30
if err := issue . LoadAttributes ( ctx ) ; err != nil {
ctx . Error ( http . StatusInternalServerError , "LoadAttributes" , err )
return
}
2022-03-22 12:33:22 +05:30
if ! issue . IsPoster ( ctx . Doer . ID ) && ! ctx . Repo . CanWrite ( unit . TypePullRequests ) {
2019-12-20 22:37:12 +05:30
ctx . Status ( http . StatusForbidden )
2016-12-02 16:40:39 +05:30
return
}
2020-05-17 02:35:19 +05:30
oldTitle := issue . Title
2016-12-02 16:40:39 +05:30
if len ( form . Title ) > 0 {
issue . Title = form . Title
}
if len ( form . Body ) > 0 {
issue . Content = form . Body
}
2019-11-03 20:16:32 +05:30
// 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 15:07:59 +05:30
if err := issues_model . UpdateIssueDeadline ( issue , deadlineUnix , ctx . Doer ) ; err != nil {
2019-12-20 22:37:12 +05:30
ctx . Error ( http . StatusInternalServerError , "UpdateIssueDeadline" , err )
2019-10-28 05:05:20 +05:30
return
}
issue . DeadlineUnix = deadlineUnix
2018-05-02 00:35:28 +05:30
}
2018-05-09 21:59:04 +05:30
// Add/delete assignees
2016-12-02 16:40:39 +05:30
2019-03-10 02:45:45 +05:30
// Deleting is done the GitHub way (quote from their api documentation):
2018-05-09 21:59:04 +05:30
// 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-10 01:27:58 +05:30
if ctx . Repo . CanWrite ( unit . TypePullRequests ) && ( form . Assignees != nil || len ( form . Assignee ) > 0 ) {
2022-03-22 12:33:22 +05:30
err = issue_service . UpdateAssignees ( issue , form . Assignee , form . Assignees , ctx . Doer )
2018-05-09 21:59:04 +05:30
if err != nil {
2021-11-24 15:19:20 +05:30
if user_model . IsErrUserNotExist ( err ) {
2019-12-20 22:37:12 +05:30
ctx . Error ( http . StatusUnprocessableEntity , "" , fmt . Sprintf ( "Assignee does not exist: [name: %s]" , err ) )
2018-05-09 21:59:04 +05:30
} else {
2019-12-20 22:37:12 +05:30
ctx . Error ( http . StatusInternalServerError , "UpdateAssignees" , err )
2018-05-09 21:59:04 +05:30
}
2016-12-02 16:40:39 +05:30
return
}
}
2018-05-09 21:59:04 +05:30
2021-11-10 01:27:58 +05:30
if ctx . Repo . CanWrite ( unit . TypePullRequests ) && form . Milestone != 0 &&
2016-12-02 16:40:39 +05:30
issue . MilestoneID != form . Milestone {
oldMilestoneID := issue . MilestoneID
issue . MilestoneID = form . Milestone
2022-03-22 12:33:22 +05:30
if err = issue_service . ChangeMilestoneAssign ( issue , ctx . Doer , oldMilestoneID ) ; err != nil {
2019-12-20 22:37:12 +05:30
ctx . Error ( http . StatusInternalServerError , "ChangeMilestoneAssign" , err )
2016-12-02 16:40:39 +05:30
return
}
}
2021-11-10 01:27:58 +05:30
if ctx . Repo . CanWrite ( unit . TypePullRequests ) && form . Labels != nil {
2022-11-19 13:42:33 +05:30
labels , err := issues_model . GetLabelsInRepoByIDs ( ctx , ctx . Repo . Repository . ID , form . Labels )
2018-11-16 16:42:44 +05:30
if err != nil {
2019-12-20 22:37:12 +05:30
ctx . Error ( http . StatusInternalServerError , "GetLabelsInRepoByIDsError" , err )
2018-11-16 16:42:44 +05:30
return
}
2020-04-20 18:40:45 +05:30
if ctx . Repo . Owner . IsOrganization ( ) {
2022-11-19 13:42:33 +05:30
orgLabels , err := issues_model . GetLabelsInOrgByIDs ( ctx , ctx . Repo . Owner . ID , form . Labels )
2020-04-20 18:40:45 +05:30
if err != nil {
ctx . Error ( http . StatusInternalServerError , "GetLabelsInOrgByIDs" , err )
return
}
labels = append ( labels , orgLabels ... )
}
2022-06-13 15:07:59 +05:30
if err = issues_model . ReplaceIssueLabels ( issue , labels , ctx . Doer ) ; err != nil {
2019-12-20 22:37:12 +05:30
ctx . Error ( http . StatusInternalServerError , "ReplaceLabelsError" , err )
2018-11-16 16:42:44 +05:30
return
}
}
2016-12-02 16:40:39 +05:30
if form . State != nil {
2021-10-03 08:41:17 +05:30
if pr . HasMerged {
ctx . Error ( http . StatusPreconditionFailed , "MergedPRState" , "cannot change state of this pull request, it was already merged" )
return
}
2021-04-09 13:10:34 +05:30
issue . IsClosed = api . StateClosed == api . StateType ( * form . State )
2020-05-17 02:35:19 +05:30
}
2022-06-13 15:07:59 +05:30
statusChangeComment , titleChanged , err := issues_model . UpdateIssueByAPI ( issue , ctx . Doer )
2020-05-17 02:35:19 +05:30
if err != nil {
2022-06-13 15:07:59 +05:30
if issues_model . IsErrDependenciesLeft ( err ) {
2020-05-17 02:35:19 +05:30
ctx . Error ( http . StatusPreconditionFailed , "DependenciesLeft" , "cannot close this pull request because it still has open dependencies" )
2016-12-02 16:40:39 +05:30
return
}
2020-05-17 02:35:19 +05:30
ctx . Error ( http . StatusInternalServerError , "UpdateIssueByAPI" , err )
return
}
if titleChanged {
2022-11-19 13:42:33 +05:30
notification . NotifyIssueChangeTitle ( ctx , ctx . Doer , issue , oldTitle )
2020-05-17 02:35:19 +05:30
}
if statusChangeComment != nil {
2023-01-25 10:17:53 +05:30
notification . NotifyIssueChangeStatus ( ctx , ctx . Doer , "" , issue , statusChangeComment , issue . IsClosed )
2016-12-02 16:40:39 +05:30
}
2020-06-08 00:43:40 +05:30
// change pull target branch
2021-10-03 08:41:17 +05:30
if ! pr . HasMerged && len ( form . Base ) != 0 && form . Base != pr . BaseBranch {
2020-06-08 00:43:40 +05:30
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 12:33:22 +05:30
if err := pull_service . ChangeTargetBranch ( ctx , pr , ctx . Doer , form . Base ) ; err != nil {
2022-06-13 15:07:59 +05:30
if issues_model . IsErrPullRequestAlreadyExists ( err ) {
2020-06-08 00:43:40 +05:30
ctx . Error ( http . StatusConflict , "IsErrPullRequestAlreadyExists" , err )
return
2022-06-13 15:07:59 +05:30
} else if issues_model . IsErrIssueIsClosed ( err ) {
2020-06-08 00:43:40 +05:30
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 13:42:33 +05:30
notification . NotifyPullRequestChangeTargetBranch ( ctx , ctx . Doer , pr , form . Base )
2020-06-08 00:43:40 +05:30
}
2022-04-28 21:15:33 +05:30
// 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 16:40:39 +05:30
// Refetch from database
2022-06-13 15:07:59 +05:30
pr , err = issues_model . GetPullRequestByIndex ( ctx , ctx . Repo . Repository . ID , pr . Index )
2016-12-02 16:40:39 +05:30
if err != nil {
2022-06-13 15:07:59 +05:30
if issues_model . IsErrPullRequestNotExist ( err ) {
2019-03-19 07:59:43 +05:30
ctx . NotFound ( )
2016-12-02 16:40:39 +05:30
} else {
2019-12-20 22:37:12 +05:30
ctx . Error ( http . StatusInternalServerError , "GetPullRequestByIndex" , err )
2016-12-02 16:40:39 +05:30
}
return
}
2017-11-13 12:32:25 +05:30
// TODO this should be 200, not 201
2022-03-22 12:33:22 +05:30
ctx . JSON ( http . StatusCreated , convert . ToAPIPullRequest ( ctx , pr , ctx . Doer ) )
2016-12-02 16:40:39 +05:30
}
// IsPullRequestMerged checks if a PR exists given an index
func IsPullRequestMerged ( ctx * context . APIContext ) {
2017-11-13 12:32:25 +05:30
// 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 09:10:42 +05:30
// format: int64
2017-11-13 12:32:25 +05:30
// required: true
// responses:
// "204":
// description: pull request has been merged
// "404":
// description: pull request has not been merged
2019-12-20 22:37:12 +05:30
2022-06-13 15:07:59 +05:30
pr , err := issues_model . GetPullRequestByIndex ( ctx , ctx . Repo . Repository . ID , ctx . ParamsInt64 ( ":index" ) )
2016-12-02 16:40:39 +05:30
if err != nil {
2022-06-13 15:07:59 +05:30
if issues_model . IsErrPullRequestNotExist ( err ) {
2019-03-19 07:59:43 +05:30
ctx . NotFound ( )
2016-12-02 16:40:39 +05:30
} else {
2019-12-20 22:37:12 +05:30
ctx . Error ( http . StatusInternalServerError , "GetPullRequestByIndex" , err )
2016-12-02 16:40:39 +05:30
}
return
}
if pr . HasMerged {
2019-12-20 22:37:12 +05:30
ctx . Status ( http . StatusNoContent )
2016-12-02 16:40:39 +05:30
}
2019-03-19 07:59:43 +05:30
ctx . NotFound ( )
2016-12-02 16:40:39 +05:30
}
// MergePullRequest merges a PR given an index
2021-01-26 21:06:53 +05:30
func MergePullRequest ( ctx * context . APIContext ) {
2017-11-13 12:32:25 +05:30
// 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 09:10:42 +05:30
// format: int64
2017-11-13 12:32:25 +05:30
// required: true
2019-02-08 13:38:38 +05:30
// - name: body
// in: body
// schema:
// $ref: "#/definitions/MergePullRequestOption"
2017-11-13 12:32:25 +05:30
// responses:
// "200":
// "$ref": "#/responses/empty"
// "405":
// "$ref": "#/responses/empty"
2019-12-20 22:37:12 +05:30
// "409":
// "$ref": "#/responses/error"
2021-04-07 01:14:05 +05:30
form := web . GetForm ( ctx ) . ( * forms . MergePullRequestForm )
2022-05-04 01:16:28 +05:30
2022-06-13 15:07:59 +05:30
pr , err := issues_model . GetPullRequestByIndex ( ctx , ctx . Repo . Repository . ID , ctx . ParamsInt64 ( ":index" ) )
2016-12-02 16:40:39 +05:30
if err != nil {
2022-06-13 15:07:59 +05:30
if issues_model . IsErrPullRequestNotExist ( err ) {
2018-01-11 03:04:17 +05:30
ctx . NotFound ( "GetPullRequestByIndex" , err )
2016-12-02 16:40:39 +05:30
} else {
2019-12-20 22:37:12 +05:30
ctx . Error ( http . StatusInternalServerError , "GetPullRequestByIndex" , err )
2016-12-02 16:40:39 +05:30
}
return
}
2022-11-19 13:42:33 +05:30
if err := pr . LoadHeadRepo ( ctx ) ; err != nil {
2020-09-21 01:50:14 +05:30
ctx . Error ( http . StatusInternalServerError , "LoadHeadRepo" , err )
2016-12-02 16:40:39 +05:30
return
}
2022-11-19 13:42:33 +05:30
if err := pr . LoadIssue ( ctx ) ; err != nil {
2019-06-13 01:11:28 +05:30
ctx . Error ( http . StatusInternalServerError , "LoadIssue" , err )
return
}
2016-12-02 16:40:39 +05:30
pr . Issue . Repo = ctx . Repo . Repository
if ctx . IsSigned {
// Update issue-user.
2022-08-25 08:01:57 +05:30
if err = activities_model . SetIssueReadBy ( ctx , pr . Issue . ID , ctx . Doer . ID ) ; err != nil {
2019-12-20 22:37:12 +05:30
ctx . Error ( http . StatusInternalServerError , "ReadBy" , err )
2016-12-02 16:40:39 +05:30
return
}
}
2022-03-31 20:23:08 +05:30
manuallMerge := repo_model . MergeStyle ( form . Do ) == repo_model . MergeStyleManuallyMerged
force := form . ForceMerge != nil && * form . ForceMerge
2019-09-18 11:09:45 +05:30
2022-05-04 01:16:28 +05:30
// start with merging by checking
2022-03-31 20:23:08 +05:30
if err := pull_service . CheckPullMergable ( ctx , ctx . Doer , & ctx . Repo . Permission , pr , manuallMerge , force ) ; err != nil {
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 03:05:06 +05:30
return
}
2021-03-04 09:11:23 +05:30
// handle manually-merged mark
2022-03-31 20:23:08 +05:30
if manuallMerge {
if err := pull_service . MergedManually ( pr , ctx . Doer , ctx . Repo . GitRepo , form . MergeCommitID ) ; err != nil {
2021-03-04 09:11:23 +05:30
if models . IsErrInvalidMergeStyle ( err ) {
2021-12-10 06:57:50 +05:30
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 09:11:23 +05:30
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 18:02:45 +05:30
if len ( form . Do ) == 0 {
form . Do = string ( repo_model . MergeStyleMerge )
}
message := strings . TrimSpace ( form . MergeTitleField )
if len ( message ) == 0 {
2022-12-29 18:10:20 +05:30
message , _ , err = pull_service . GetDefaultMergeMessage ( ctx , ctx . Repo . GitRepo , pr , repo_model . MergeStyle ( form . Do ) )
2022-05-08 18:02:45 +05:30
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 03:05:06 +05:30
}
2022-05-07 22:35:52 +05:30
if form . MergeWhenChecksSucceed {
2022-05-08 18:02:45 +05:30
scheduled , err := automerge . ScheduleAutoMerge ( ctx , ctx . Doer , pr , repo_model . MergeStyle ( form . Do ) , message )
2022-05-07 22:35:52 +05:30
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 21:19:00 +05:30
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-06 00:26:50 +05:30
if models . IsErrInvalidMergeStyle ( err ) {
2021-12-10 06:57:50 +05:30
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 14:12:51 +05:30
} 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 09:43:18 +05:30
} else if git . IsErrPushOutOfDate ( err ) {
2019-12-20 22:37:12 +05:30
ctx . Error ( http . StatusConflict , "Merge" , "merge push out of date" )
2021-12-20 06:02:54 +05:30
} else if models . IsErrSHADoesNotMatch ( err ) {
ctx . Error ( http . StatusConflict , "Merge" , "head out of date" )
2020-03-28 09:43:18 +05:30
} else if git . IsErrPushRejected ( err ) {
errPushRej := err . ( * git . ErrPushRejected )
2020-02-22 18:38:48 +05:30
if len ( errPushRej . Message ) == 0 {
ctx . Error ( http . StatusConflict , "Merge" , "PushRejected without remote error message" )
2022-05-04 01:16:28 +05:30
} else {
ctx . Error ( http . StatusConflict , "Merge" , "PushRejected with remote message: " + errPushRej . Message )
2020-02-22 18:38:48 +05:30
}
2022-05-04 01:16:28 +05:30
} else {
ctx . Error ( http . StatusInternalServerError , "Merge" , err )
2018-01-06 00:26:50 +05:30
}
2016-12-02 16:40:39 +05:30
return
}
log . Trace ( "Pull request merged: %d" , pr . ID )
2021-07-13 04:56:25 +05:30
if form . DeleteBranchAfterMerge {
2022-01-04 01:15:58 +05:30
// Don't cleanup when there are other PR's that use this branch as head branch.
2022-06-13 15:07:59 +05:30
exist , err := issues_model . HasUnmergedPullRequestsByHeadInfo ( ctx , pr . HeadRepoID , pr . HeadBranch )
2022-01-04 01:15:58 +05:30
if err != nil {
ctx . ServerError ( "HasUnmergedPullRequestsByHeadInfo" , err )
return
}
if exist {
ctx . Status ( http . StatusOK )
return
}
2021-07-13 04:56:25 +05:30
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-30 00:43:41 +05:30
headRepo , err = git . OpenRepository ( ctx , pr . HeadRepo . RepoPath ( ) )
2021-07-13 04:56:25 +05:30
if err != nil {
ctx . ServerError ( fmt . Sprintf ( "OpenRepository[%s]" , pr . HeadRepo . RepoPath ( ) ) , err )
return
}
defer headRepo . Close ( )
}
2022-03-22 12:33:22 +05:30
if err := repo_service . DeleteBranch ( ctx . Doer , pr . HeadRepo , headRepo , pr . HeadBranch ) ; err != nil {
2021-07-13 04:56:25 +05:30
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 13:30:22 +05:30
case errors . Is ( err , git_model . ErrBranchIsProtected ) :
2021-07-13 04:56:25 +05:30
ctx . Error ( http . StatusForbidden , "IsProtectedBranch" , fmt . Errorf ( "branch protected" ) )
default :
ctx . Error ( http . StatusInternalServerError , "DeleteBranch" , err )
}
return
}
2022-06-13 15:07:59 +05:30
if err := issues_model . AddDeletePRBranchComment ( ctx , ctx . Doer , pr . BaseRepo , pr . Issue . ID , pr . HeadBranch ) ; err != nil {
2021-07-13 04:56:25 +05:30
// Do not fail here as branch has already been deleted
log . Error ( "DeleteBranch: %v" , err )
}
}
2019-12-20 22:37:12 +05:30
ctx . Status ( http . StatusOK )
2016-12-02 16:40:39 +05:30
}
2021-12-10 06:57:50 +05:30
func parseCompareInfo ( ctx * context . APIContext , form api . CreatePullRequestOption ) ( * user_model . User , * repo_model . Repository , * git . Repository , * git . CompareInfo , string , string ) {
2016-12-02 16:40:39 +05:30
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 15:19:20 +05:30
headUser * user_model . User
2016-12-02 16:40:39 +05:30
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 19:38:52 +05:30
headUser , err = user_model . GetUserByName ( ctx , headInfos [ 0 ] )
2016-12-02 16:40:39 +05:30
if err != nil {
2021-11-24 15:19:20 +05:30
if user_model . IsErrUserNotExist ( err ) {
2019-05-07 22:50:23 +05:30
ctx . NotFound ( "GetUserByName" )
2016-12-02 16:40:39 +05:30
} else {
2020-09-21 01:50:14 +05:30
ctx . Error ( http . StatusInternalServerError , "GetUserByName" , err )
2016-12-02 16:40:39 +05:30
}
return nil , nil , nil , nil , "" , ""
}
headBranch = headInfos [ 1 ]
} else {
2019-03-19 07:59:43 +05:30
ctx . NotFound ( )
2016-12-02 16:40:39 +05:30
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 22:50:23 +05:30
ctx . NotFound ( "IsBranchExist" )
2016-12-02 16:40:39 +05:30
return nil , nil , nil , nil , "" , ""
}
// Check if current user has fork of repository or in the same repository.
2021-12-12 21:18:20 +05:30
headRepo := repo_model . GetForkedRepo ( headUser . ID , baseRepo . ID )
2021-11-22 20:51:55 +05:30
if headRepo == nil && ! isSameRepo {
2016-12-02 16:40:39 +05:30
log . Trace ( "parseCompareInfo[%d]: does not have fork or in same repository" , baseRepo . ID )
2021-11-22 20:51:55 +05:30
ctx . NotFound ( "GetForkedRepo" )
2016-12-02 16:40:39 +05:30
return nil , nil , nil , nil , "" , ""
}
var headGitRepo * git . Repository
if isSameRepo {
headRepo = ctx . Repo . Repository
headGitRepo = ctx . Repo . GitRepo
} else {
2022-03-30 00:43:41 +05:30
headGitRepo , err = git . OpenRepository ( ctx , repo_model . RepoPath ( headUser . Name , headRepo . Name ) )
2016-12-02 16:40:39 +05:30
if err != nil {
2019-12-20 22:37:12 +05:30
ctx . Error ( http . StatusInternalServerError , "OpenRepository" , err )
2016-12-02 16:40:39 +05:30
return nil , nil , nil , nil , "" , ""
}
}
2019-05-07 22:50:23 +05:30
// user should have permission to read baseRepo's codes and pulls, NOT headRepo's
2022-05-11 15:39:36 +05:30
permBase , err := access_model . GetUserRepoPermission ( ctx , baseRepo , ctx . Doer )
2018-11-28 16:56:14 +05:30
if err != nil {
2019-11-13 12:31:19 +05:30
headGitRepo . Close ( )
2020-09-21 01:50:14 +05:30
ctx . Error ( http . StatusInternalServerError , "GetUserRepoPermission" , err )
2018-11-28 16:56:14 +05:30
return nil , nil , nil , nil , "" , ""
}
2021-11-10 01:27:58 +05:30
if ! permBase . CanReadIssuesOrPulls ( true ) || ! permBase . CanRead ( unit . TypeCode ) {
2019-04-23 02:10:51 +05:30
if log . IsTrace ( ) {
2019-05-07 22:50:23 +05:30
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 12:33:22 +05:30
ctx . Doer ,
2019-05-07 22:50:23 +05:30
baseRepo ,
permBase )
}
2019-11-13 12:31:19 +05:30
headGitRepo . Close ( )
2019-05-07 22:50:23 +05:30
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 15:39:36 +05:30
permHead , err := access_model . GetUserRepoPermission ( ctx , headRepo , ctx . Doer )
2019-05-07 22:50:23 +05:30
if err != nil {
2019-11-13 12:31:19 +05:30
headGitRepo . Close ( )
2020-09-21 01:50:14 +05:30
ctx . Error ( http . StatusInternalServerError , "GetUserRepoPermission" , err )
2019-05-07 22:50:23 +05:30
return nil , nil , nil , nil , "" , ""
}
2021-11-10 01:27:58 +05:30
if ! permHead . CanRead ( unit . TypeCode ) {
2019-05-07 22:50:23 +05:30
if log . IsTrace ( ) {
log . Trace ( "Permission Denied: User: %-v cannot read code in Repo: %-v\nUser in headRepo has Permissions: %-+v" ,
2022-03-22 12:33:22 +05:30
ctx . Doer ,
2019-04-23 02:10:51 +05:30
headRepo ,
2019-05-07 22:50:23 +05:30
permHead )
2019-04-23 02:10:51 +05:30
}
2019-11-13 12:31:19 +05:30
headGitRepo . Close ( )
2019-05-07 22:50:23 +05:30
ctx . NotFound ( "Can't read headRepo UnitTypeCode" )
2016-12-02 16:40:39 +05:30
return nil , nil , nil , nil , "" , ""
}
// Check if head branch is valid.
if ! headGitRepo . IsBranchExist ( headBranch ) {
2019-11-13 12:31:19 +05:30
headGitRepo . Close ( )
2019-03-19 07:59:43 +05:30
ctx . NotFound ( )
2016-12-02 16:40:39 +05:30
return nil , nil , nil , nil , "" , ""
}
2022-01-18 13:15:43 +05:30
compareInfo , err := headGitRepo . GetCompareInfo ( repo_model . RepoPath ( baseRepo . Owner . Name , baseRepo . Name ) , baseBranch , headBranch , false , false )
2016-12-02 16:40:39 +05:30
if err != nil {
2019-11-13 12:31:19 +05:30
headGitRepo . Close ( )
2019-12-20 22:37:12 +05:30
ctx . Error ( http . StatusInternalServerError , "GetCompareInfo" , err )
2016-12-02 16:40:39 +05:30
return nil , nil , nil , nil , "" , ""
}
2019-06-08 01:59:29 +05:30
return headUser , headRepo , headGitRepo , compareInfo , baseBranch , headBranch
2016-12-02 16:40:39 +05:30
}
2020-08-05 02:25:22 +05:30
// 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 19:33:45 +05:30
// - name: style
// in: query
// description: how to update pull request
// type: string
// enum: [merge, rebase]
2020-08-05 02:25:22 +05:30
// responses:
// "200":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
// "409":
// "$ref": "#/responses/error"
// "422":
// "$ref": "#/responses/validationError"
2022-06-13 15:07:59 +05:30
pr , err := issues_model . GetPullRequestByIndex ( ctx , ctx . Repo . Repository . ID , ctx . ParamsInt64 ( ":index" ) )
2020-08-05 02:25:22 +05:30
if err != nil {
2022-06-13 15:07:59 +05:30
if issues_model . IsErrPullRequestNotExist ( err ) {
2020-08-05 02:25:22 +05:30
ctx . NotFound ( )
} else {
ctx . Error ( http . StatusInternalServerError , "GetPullRequestByIndex" , err )
}
return
}
if pr . HasMerged {
ctx . Error ( http . StatusUnprocessableEntity , "UpdatePullRequest" , err )
return
}
2022-11-19 13:42:33 +05:30
if err = pr . LoadIssue ( ctx ) ; err != nil {
2020-08-05 02:25:22 +05:30
ctx . Error ( http . StatusInternalServerError , "LoadIssue" , err )
return
}
if pr . Issue . IsClosed {
ctx . Error ( http . StatusUnprocessableEntity , "UpdatePullRequest" , err )
return
}
2022-11-19 13:42:33 +05:30
if err = pr . LoadBaseRepo ( ctx ) ; err != nil {
2020-08-05 02:25:22 +05:30
ctx . Error ( http . StatusInternalServerError , "LoadBaseRepo" , err )
return
}
2022-11-19 13:42:33 +05:30
if err = pr . LoadHeadRepo ( ctx ) ; err != nil {
2020-08-05 02:25:22 +05:30
ctx . Error ( http . StatusInternalServerError , "LoadHeadRepo" , err )
return
}
2021-08-31 19:33:45 +05:30
rebase := ctx . FormString ( "style" ) == "rebase"
2022-04-28 17:18:48 +05:30
allowedUpdateByMerge , allowedUpdateByRebase , err := pull_service . IsUserAllowedToUpdate ( ctx , pr , ctx . Doer )
2020-08-05 02:25:22 +05:30
if err != nil {
ctx . Error ( http . StatusInternalServerError , "IsUserAllowedToMerge" , err )
return
}
2021-08-31 19:33:45 +05:30
if ( ! allowedUpdateByMerge && ! rebase ) || ( rebase && ! allowedUpdateByRebase ) {
2020-08-05 02:25:22 +05:30
ctx . Status ( http . StatusForbidden )
return
}
// default merge commit message
message := fmt . Sprintf ( "Merge branch '%s' into %s" , pr . BaseBranch , pr . HeadBranch )
2022-03-22 12:33:22 +05:30
if err = pull_service . Update ( ctx , pr , ctx . Doer , message , rebase ) ; err != nil {
2020-08-05 02:25:22 +05:30
if models . IsErrMergeConflicts ( err ) {
ctx . Error ( http . StatusConflict , "Update" , "merge failed because of conflict" )
return
2021-09-05 15:00:40 +05:30
} else if models . IsErrRebaseConflicts ( err ) {
ctx . Error ( http . StatusConflict , "Update" , "rebase failed because of conflict" )
return
2020-08-05 02:25:22 +05:30
}
ctx . Error ( http . StatusInternalServerError , "pull_service.Update" , err )
return
}
ctx . Status ( http . StatusOK )
}
2021-07-02 17:49:57 +05:30
2022-05-07 22:35:52 +05:30
// 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 15:07:59 +05:30
pull , err := issues_model . GetPullRequestByIndex ( ctx , ctx . Repo . Repository . ID , pullIndex )
2022-05-07 22:35:52 +05:30
if err != nil {
2022-06-13 15:07:59 +05:30
if issues_model . IsErrPullRequestNotExist ( err ) {
2022-05-07 22:35:52 +05:30
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 19:38:52 +05:30
allowed , err := access_model . IsUserRepoAdmin ( ctx , ctx . Repo . Repository , ctx . Doer )
2022-05-07 22:35:52 +05:30
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 19:16:34 +05:30
if err := automerge . RemoveScheduledAutoMerge ( ctx , ctx . Doer , pull ) ; err != nil {
2022-05-07 22:35:52 +05:30
ctx . InternalServerError ( err )
} else {
ctx . Status ( http . StatusNoContent )
}
}
2021-07-02 17:49:57 +05:30
// 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 15:07:59 +05:30
pr , err := issues_model . GetPullRequestByIndex ( ctx , ctx . Repo . Repository . ID , ctx . ParamsInt64 ( ":index" ) )
2021-07-02 17:49:57 +05:30
if err != nil {
2022-06-13 15:07:59 +05:30
if issues_model . IsErrPullRequestNotExist ( err ) {
2021-07-02 17:49:57 +05:30
ctx . NotFound ( )
} else {
ctx . Error ( http . StatusInternalServerError , "GetPullRequestByIndex" , err )
}
return
}
2022-11-19 13:42:33 +05:30
if err := pr . LoadBaseRepo ( ctx ) ; err != nil {
2021-07-02 17:49:57 +05:30
ctx . InternalServerError ( err )
return
}
var prInfo * git . CompareInfo
2022-01-20 04:56:57 +05:30
baseGitRepo , closer , err := git . RepositoryFromContextOrOpen ( ctx , pr . BaseRepo . RepoPath ( ) )
2021-07-02 17:49:57 +05:30
if err != nil {
ctx . ServerError ( "OpenRepository" , err )
return
}
2022-01-20 04:56:57 +05:30
defer closer . Close ( )
2021-07-02 17:49:57 +05:30
if pr . HasMerged {
2022-01-18 13:15:43 +05:30
prInfo , err = baseGitRepo . GetCompareInfo ( pr . BaseRepo . RepoPath ( ) , pr . MergeBase , pr . GetGitRefName ( ) , false , false )
2021-07-02 17:49:57 +05:30
} else {
2022-01-18 13:15:43 +05:30
prInfo , err = baseGitRepo . GetCompareInfo ( pr . BaseRepo . RepoPath ( ) , pr . BaseBranch , pr . GetGitRefName ( ) , false , false )
2021-07-02 17:49:57 +05:30
}
if err != nil {
ctx . ServerError ( "GetCompareInfo" , err )
return
}
commits := prInfo . Commits
listOptions := utils . GetListOptions ( ctx )
2021-08-09 23:38:51 +05:30
totalNumberOfCommits := len ( commits )
2021-07-02 17:49:57 +05:30
totalNumberOfPages := int ( math . Ceil ( float64 ( totalNumberOfCommits ) / float64 ( listOptions . PageSize ) ) )
2021-11-24 15:19:20 +05:30
userCache := make ( map [ string ] * user_model . User )
2021-07-02 17:49:57 +05:30
start , end := listOptions . GetStartEnd ( )
if end > totalNumberOfCommits {
end = totalNumberOfCommits
}
2021-08-09 23:38:51 +05:30
apiCommits := make ( [ ] * api . Commit , 0 , end - start )
for i := start ; i < end ; i ++ {
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 19:07:34 +05:30
apiCommit , err := convert . ToCommit ( ctx , ctx . Repo . Repository , baseGitRepo , commits [ i ] , userCache , true )
2021-07-02 17:49:57 +05:30
if err != nil {
ctx . ServerError ( "toCommit" , err )
return
}
2021-08-09 23:38:51 +05:30
apiCommits = append ( apiCommits , apiCommit )
2021-07-02 17:49:57 +05:30
}
2021-08-12 18:13:08 +05:30
ctx . SetLinkHeader ( totalNumberOfCommits , listOptions . PageSize )
ctx . SetTotalCountHeader ( int64 ( totalNumberOfCommits ) )
2021-07-02 17:49:57 +05:30
2021-12-15 12:29:57 +05:30
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 18:13:08 +05:30
ctx . AppendAccessControlExposeHeaders ( "X-Page" , "X-PerPage" , "X-PageCount" , "X-HasMore" )
2021-07-02 17:49:57 +05:30
ctx . JSON ( http . StatusOK , & apiCommits )
}
2022-09-29 07:57:20 +05:30
// 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 13:42:33 +05:30
if err := pr . LoadBaseRepo ( ctx ) ; err != nil {
2022-09-29 07:57:20 +05:30
ctx . InternalServerError ( err )
return
}
2022-11-19 13:42:33 +05:30
if err := pr . LoadHeadRepo ( ctx ) ; err != nil {
2022-09-29 07:57:20 +05:30
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
maxLines , maxFiles := setting . Git . MaxGitDiffLines , setting . Git . MaxGitDiffFiles
diff , err := gitdiff . GetDiff ( baseGitRepo ,
& gitdiff . DiffOptions {
BeforeCommitID : startCommitID ,
AfterCommitID : endCommitID ,
SkipTo : ctx . FormString ( "skip-to" ) ,
MaxLines : maxLines ,
MaxLineCharacters : setting . Git . MaxGitDiffLineCharacters ,
MaxFiles : maxFiles ,
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 21:16:11 +05:30
lenFiles := end - start
if lenFiles < 0 {
lenFiles = 0
}
apiFiles := make ( [ ] * api . ChangedFile , 0 , lenFiles )
2022-09-29 07:57:20 +05:30
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 )
}