Files

1625 lines
47 KiB
Go
Raw Permalink Normal View History

2016-12-02 12:10:39 +01:00
// Copyright 2016 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
2016-12-02 12:10:39 +01:00
package repo
import (
"errors"
2016-12-02 12:10:39 +01:00
"fmt"
2021-07-02 14:19:57 +02:00
"math"
"net/http"
2021-07-02 14:19:57 +02:00
"strconv"
2016-12-02 12:10:39 +01:00
"strings"
2019-11-03 15:46:32 +01:00
"time"
2016-12-02 12:10:39 +01:00
activities_model "code.gitea.io/gitea/models/activities"
git_model "code.gitea.io/gitea/models/git"
2022-04-08 17:11:15 +08:00
issues_model "code.gitea.io/gitea/models/issues"
access_model "code.gitea.io/gitea/models/perm/access"
pull_model "code.gitea.io/gitea/models/pull"
repo_model "code.gitea.io/gitea/models/repo"
2021-11-10 03:57:58 +08:00
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
2024-03-21 23:07:35 +08:00
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/git/gitcmd"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/graceful"
2016-12-02 12:10:39 +01:00
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
2019-05-11 18:21:34 +08:00
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
2021-01-26 23:36:53 +08:00
"code.gitea.io/gitea/modules/web"
2020-01-24 19:00:29 +00:00
"code.gitea.io/gitea/routers/api/v1/utils"
2025-12-17 13:21:04 -08:00
"code.gitea.io/gitea/routers/common"
2021-12-10 16:14:24 +08:00
asymkey_service "code.gitea.io/gitea/services/asymkey"
"code.gitea.io/gitea/services/automerge"
"code.gitea.io/gitea/services/context"
2022-12-29 03:57:15 +01:00
"code.gitea.io/gitea/services/convert"
"code.gitea.io/gitea/services/forms"
git_service "code.gitea.io/gitea/services/git"
"code.gitea.io/gitea/services/gitdiff"
issue_service "code.gitea.io/gitea/services/issue"
notify_service "code.gitea.io/gitea/services/notify"
pull_service "code.gitea.io/gitea/services/pull"
repo_service "code.gitea.io/gitea/services/repository"
2016-12-02 12:10:39 +01:00
)
// ListPullRequests returns a list of all PRs
2021-01-26 23:36:53 +08:00
func ListPullRequests(ctx *context.APIContext) {
2017-11-12 23:02:25 -08: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
2017-11-12 23:02:25 -08:00
// type: string
// required: true
// - name: repo
// in: path
// description: Name of the repo
2017-11-12 23:02:25 -08:00
// type: string
// required: true
// - name: base_branch
// in: query
// description: Filter by target base branch of the pull request
// type: string
// - name: state
// in: query
// description: State of pull request
// type: string
// enum: [open, closed, all]
// default: open
// - name: sort
// in: query
// description: Type of sort
// type: string
// enum: [oldest, recentupdate, recentclose, 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
// - name: poster
// in: query
// description: Filter by pull request author
// type: string
2020-01-24 19:00:29 +00:00
// - name: page
// in: query
// description: Page number of results to return (1-based)
2020-01-24 19:00:29 +00:00
// type: integer
// minimum: 1
// default: 1
2020-01-24 19:00:29 +00:00
// - name: limit
// in: query
// description: Page size of results
2020-01-24 19:00:29 +00:00
// type: integer
// minimum: 0
2017-11-12 23:02:25 -08:00
// responses:
// "200":
// "$ref": "#/responses/PullRequestList"
// "404":
// "$ref": "#/responses/notFound"
// "500":
// "$ref": "#/responses/error"
2019-12-20 18:07:12 +01:00
2024-03-21 23:07:35 +08:00
labelIDs, err := base.StringsToInt64s(ctx.FormStrings("labels"))
if err != nil {
2025-02-18 04:41:03 +08:00
ctx.APIErrorInternal(err)
2024-03-21 23:07:35 +08:00
return
}
var posterID int64
if posterStr := ctx.FormString("poster"); posterStr != "" {
poster, err := user_model.GetUserByName(ctx, posterStr)
if err != nil {
if user_model.IsErrUserNotExist(err) {
2025-02-17 14:13:17 +08:00
ctx.APIError(http.StatusBadRequest, err)
} else {
2025-02-18 04:41:03 +08:00
ctx.APIErrorInternal(err)
}
return
}
posterID = poster.ID
}
2020-01-24 19:00:29 +00:00
listOptions := utils.GetListOptions(ctx)
prs, maxResults, err := issues_model.PullRequests(ctx, ctx.Repo.Repository.ID, &issues_model.PullRequestsOptions{
2020-01-24 19:00:29 +00:00
ListOptions: listOptions,
State: ctx.FormTrim("state"),
SortType: ctx.FormTrim("sort"),
2024-03-21 23:07:35 +08:00
Labels: labelIDs,
MilestoneID: ctx.FormInt64("milestone"),
PosterID: posterID,
BaseBranch: ctx.FormTrim("base_branch"),
2016-12-02 12:10:39 +01:00
})
if err != nil {
2025-02-18 04:41:03 +08:00
ctx.APIErrorInternal(err)
2016-12-02 12:10:39 +01:00
return
}
apiPrs, err := convert.ToAPIPullRequests(ctx, ctx.Repo.Repository, prs, ctx.Doer)
if err != nil {
2025-02-18 04:41:03 +08:00
ctx.APIErrorInternal(err)
return
}
2016-12-02 12:10:39 +01:00
2026-03-08 15:35:50 +01:00
ctx.SetLinkHeader(maxResults, listOptions.PageSize)
2021-08-12 14:43:08 +02:00
ctx.SetTotalCountHeader(maxResults)
2019-12-20 18:07:12 +01:00
ctx.JSON(http.StatusOK, &apiPrs)
2016-12-02 12:10:39 +01:00
}
// GetPullRequest returns a single PR based on index
func GetPullRequest(ctx *context.APIContext) {
2017-11-12 23:02:25 -08: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
// format: int64
2017-11-12 23:02:25 -08:00
// required: true
// responses:
// "200":
// "$ref": "#/responses/PullRequest"
2020-01-09 12:56:32 +01:00
// "404":
// "$ref": "#/responses/notFound"
2019-12-20 18:07:12 +01:00
2024-12-24 21:47:45 +08:00
pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
2016-12-02 12:10:39 +01:00
if err != nil {
if issues_model.IsErrPullRequestNotExist(err) {
2025-02-17 14:13:17 +08:00
ctx.APIErrorNotFound()
2016-12-02 12:10:39 +01:00
} else {
2025-02-18 04:41:03 +08:00
ctx.APIErrorInternal(err)
2016-12-02 12:10:39 +01:00
}
return
}
if err = pr.LoadBaseRepo(ctx); err != nil {
2025-02-18 04:41:03 +08:00
ctx.APIErrorInternal(err)
2017-11-04 11:10:01 -07:00
return
}
if err = pr.LoadHeadRepo(ctx); err != nil {
2025-02-18 04:41:03 +08:00
ctx.APIErrorInternal(err)
2017-11-04 11:10:01 -07:00
return
}
// Consider API access a view for delayed checking.
pull_service.StartPullRequestCheckOnView(ctx, pr)
2022-03-22 08:03:22 +01:00
ctx.JSON(http.StatusOK, convert.ToAPIPullRequest(ctx, pr, ctx.Doer))
2016-12-02 12:10:39 +01:00
}
2024-02-26 03:39:01 +01:00
// GetPullRequest returns a single PR based on index
func GetPullRequestByBaseHead(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/pulls/{base}/{head} repository repoGetPullRequestByBaseHead
// ---
// summary: Get a pull request by base and head
// 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: base
// in: path
// description: base of the pull request to get
// type: string
// required: true
// - name: head
// in: path
// description: head of the pull request to get
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/PullRequest"
// "404":
// "$ref": "#/responses/notFound"
var headRepoID int64
var headBranch string
2024-06-19 06:32:45 +08:00
head := ctx.PathParam("*")
2024-02-26 03:39:01 +01:00
if strings.Contains(head, ":") {
split := strings.SplitN(head, ":", 2)
headBranch = split[1]
var owner, name string
if strings.Contains(split[0], "/") {
split = strings.Split(split[0], "/")
owner = split[0]
name = split[1]
} else {
owner = split[0]
name = ctx.Repo.Repository.Name
}
repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, owner, name)
if err != nil {
if repo_model.IsErrRepoNotExist(err) {
2025-02-17 14:13:17 +08:00
ctx.APIErrorNotFound()
2024-02-26 03:39:01 +01:00
} else {
2025-02-18 04:41:03 +08:00
ctx.APIErrorInternal(err)
2024-02-26 03:39:01 +01:00
}
return
}
headRepoID = repo.ID
} else {
headRepoID = ctx.Repo.Repository.ID
headBranch = head
}
2024-12-24 21:47:45 +08:00
pr, err := issues_model.GetPullRequestByBaseHeadInfo(ctx, ctx.Repo.Repository.ID, headRepoID, ctx.PathParam("base"), headBranch)
2024-02-26 03:39:01 +01:00
if err != nil {
if issues_model.IsErrPullRequestNotExist(err) {
2025-02-17 14:13:17 +08:00
ctx.APIErrorNotFound()
2024-02-26 03:39:01 +01:00
} else {
2025-02-18 04:41:03 +08:00
ctx.APIErrorInternal(err)
2024-02-26 03:39:01 +01:00
}
return
}
if err = pr.LoadBaseRepo(ctx); err != nil {
2025-02-18 04:41:03 +08:00
ctx.APIErrorInternal(err)
2024-02-26 03:39:01 +01:00
return
}
if err = pr.LoadHeadRepo(ctx); err != nil {
2025-02-18 04:41:03 +08:00
ctx.APIErrorInternal(err)
2024-02-26 03:39:01 +01:00
return
}
// Consider API access a view for delayed checking.
pull_service.StartPullRequestCheckOnView(ctx, pr)
2024-02-26 03:39:01 +01:00
ctx.JSON(http.StatusOK, convert.ToAPIPullRequest(ctx, pr, ctx.Doer))
}
// 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
// ---
// summary: Get a pull request diff or patch
// 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
// - name: diffType
// in: path
// description: whether the output is diff or patch
// type: string
// enum: [diff, patch]
// required: true
// - name: binary
// in: query
// description: whether to include binary file changes. if true, the diff is applicable with `git apply`
// type: boolean
// responses:
// "200":
// "$ref": "#/responses/string"
// "404":
// "$ref": "#/responses/notFound"
2024-12-24 21:47:45 +08:00
pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
if issues_model.IsErrPullRequestNotExist(err) {
2025-02-17 14:13:17 +08:00
ctx.APIErrorNotFound()
} else {
2025-02-17 14:13:17 +08:00
ctx.APIErrorInternal(err)
}
return
}
var patch bool
2024-12-24 21:47:45 +08:00
if ctx.PathParam("diffType") == "diff" {
patch = false
} else {
patch = true
}
binary := ctx.FormBool("binary")
if err := pull_service.DownloadDiffOrPatch(ctx, pr, ctx, patch, binary); err != nil {
2025-02-17 14:13:17 +08:00
ctx.APIErrorInternal(err)
return
}
}
2016-12-02 12:10:39 +01:00
// CreatePullRequest does what it says
2021-01-26 23:36:53 +08:00
func CreatePullRequest(ctx *context.APIContext) {
2017-11-12 23:02:25 -08: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"
2024-03-04 09:16:03 +01:00
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
2019-12-20 18:07:12 +01:00
// "409":
// "$ref": "#/responses/error"
// "422":
// "$ref": "#/responses/validationError"
// "423":
// "$ref": "#/responses/repoArchivedError"
2019-12-20 18:07:12 +01:00
2021-01-26 23:36:53 +08:00
form := *web.GetForm(ctx).(*api.CreatePullRequestOption)
2020-10-26 10:05:27 +01:00
if form.Head == form.Base {
2025-02-17 14:13:17 +08:00
ctx.APIError(http.StatusUnprocessableEntity, "Invalid PullRequest: There are no changes between the head and the base")
2020-10-26 10:05:27 +01:00
return
}
2016-12-02 12:10:39 +01:00
var (
repo = ctx.Repo.Repository
labelIDs []int64
milestoneID int64
)
// Get repo/branch information
compareResult, closer := parseCompareInfo(ctx, form.Base+".."+form.Head)
2016-12-02 12:10:39 +01:00
if ctx.Written() {
return
}
defer closer()
if !compareResult.BaseRef.IsBranch() || !compareResult.HeadRef.IsBranch() {
2025-02-17 14:13:17 +08:00
ctx.APIError(http.StatusUnprocessableEntity, "Invalid PullRequest: base and head must be branches")
return
}
2016-12-02 12:10:39 +01:00
// Check if another PR exists with the same targets
existingPr, err := issues_model.GetUnmergedPullRequest(ctx, compareResult.HeadRepo.ID, ctx.Repo.Repository.ID,
compareResult.HeadRef.ShortName(), compareResult.BaseRef.ShortName(),
issues_model.PullRequestFlowGithub,
)
2016-12-02 12:10:39 +01:00
if err != nil {
if !issues_model.IsErrPullRequestNotExist(err) {
2025-02-18 04:41:03 +08:00
ctx.APIErrorInternal(err)
2016-12-02 12:10:39 +01:00
return
}
} else {
err = issues_model.ErrPullRequestAlreadyExists{
2016-12-02 12:10:39 +01:00
ID: existingPr.ID,
IssueID: existingPr.Index,
HeadRepoID: existingPr.HeadRepoID,
BaseRepoID: existingPr.BaseRepoID,
HeadBranch: existingPr.HeadBranch,
BaseBranch: existingPr.BaseBranch,
}
2025-02-17 14:13:17 +08:00
ctx.APIError(http.StatusConflict, err)
2016-12-02 12:10:39 +01:00
return
}
if len(form.Labels) > 0 {
labels, err := issues_model.GetLabelsInRepoByIDs(ctx, ctx.Repo.Repository.ID, form.Labels)
2016-12-02 12:10:39 +01:00
if err != nil {
2025-02-18 04:41:03 +08:00
ctx.APIErrorInternal(err)
2016-12-02 12:10:39 +01:00
return
}
2023-08-29 11:47:26 -04:00
labelIDs = make([]int64, 0, len(labels))
for _, label := range labels {
labelIDs = append(labelIDs, label.ID)
2016-12-02 12:10:39 +01:00
}
if ctx.Repo.Owner.IsOrganization() {
orgLabels, err := issues_model.GetLabelsInOrgByIDs(ctx, ctx.Repo.Owner.ID, form.Labels)
if err != nil {
2025-02-18 04:41:03 +08:00
ctx.APIErrorInternal(err)
return
}
2023-08-29 11:47:26 -04:00
orgLabelIDs := make([]int64, 0, len(orgLabels))
for _, orgLabel := range orgLabels {
orgLabelIDs = append(orgLabelIDs, orgLabel.ID)
}
2023-08-29 11:47:26 -04:00
labelIDs = append(labelIDs, orgLabelIDs...)
}
2016-12-02 12:10:39 +01:00
}
if form.Milestone > 0 {
2022-04-08 17:11:15 +08:00
milestone, err := issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, form.Milestone)
2016-12-02 12:10:39 +01:00
if err != nil {
2022-04-08 17:11:15 +08:00
if issues_model.IsErrMilestoneNotExist(err) {
2025-02-17 14:13:17 +08:00
ctx.APIErrorNotFound()
2016-12-02 12:10:39 +01:00
} else {
2025-02-18 04:41:03 +08:00
ctx.APIErrorInternal(fmt.Errorf("GetMilestoneByRepoID: %w", err))
2016-12-02 12:10:39 +01:00
}
return
}
milestoneID = milestone.ID
}
var deadlineUnix timeutil.TimeStamp
2018-05-01 21:05:28 +02:00
if form.Deadline != nil {
deadlineUnix = timeutil.TimeStamp(form.Deadline.Unix())
2018-05-01 21:05:28 +02:00
}
unitPullRequest, err := ctx.Repo.Repository.GetUnit(ctx, unit.TypePullRequests)
if err != nil {
ctx.APIErrorInternal(err)
2026-01-10 02:58:21 +08:00
return
}
prIssue := &issues_model.Issue{
2018-05-01 21:05:28 +02:00
RepoID: repo.ID,
Title: form.Title,
2022-03-22 08:03:22 +01:00
PosterID: ctx.Doer.ID,
Poster: ctx.Doer,
2018-05-01 21:05:28 +02:00
MilestoneID: milestoneID,
IsPull: true,
Content: form.Body,
DeadlineUnix: deadlineUnix,
2016-12-02 12:10:39 +01:00
}
pr := &issues_model.PullRequest{
HeadRepoID: compareResult.HeadRepo.ID,
BaseRepoID: repo.ID,
HeadBranch: compareResult.HeadRef.ShortName(),
BaseBranch: compareResult.BaseRef.ShortName(),
HeadRepo: compareResult.HeadRepo,
BaseRepo: repo,
MergeBase: compareResult.MergeBase,
Type: issues_model.PullRequestGitea,
2016-12-02 12:10:39 +01:00
}
pr.AllowMaintainerEdit = optional.FromPtr(form.AllowMaintainerEdit).ValueOrDefault(unitPullRequest.PullRequestsConfig().DefaultAllowMaintainerEdit)
2018-05-09 18:29:04 +02:00
// Get all assignee IDs
assigneeIDs, err := issues_model.MakeIDsFromAPIAssigneesToAdd(ctx, form.Assignee, form.Assignees)
2018-05-09 18:29:04 +02:00
if err != nil {
if user_model.IsErrUserNotExist(err) {
2025-02-17 14:13:17 +08:00
ctx.APIError(http.StatusUnprocessableEntity, fmt.Sprintf("Assignee does not exist: [name: %s]", err))
2018-05-09 18:29:04 +02:00
} else {
2025-02-18 04:41:03 +08:00
ctx.APIErrorInternal(err)
2018-05-09 18:29:04 +02:00
}
return
}
// Check if the passed assignees is assignable
for _, aID := range assigneeIDs {
assignee, err := user_model.GetUserByID(ctx, aID)
if err != nil {
2025-02-18 04:41:03 +08:00
ctx.APIErrorInternal(err)
return
}
2018-05-09 18:29:04 +02:00
valid, err := access_model.CanBeAssigned(ctx, assignee, repo, true)
if err != nil {
2025-02-18 04:41:03 +08:00
ctx.APIErrorInternal(err)
return
}
if !valid {
2025-02-17 14:13:17 +08:00
ctx.APIError(http.StatusUnprocessableEntity, repo_model.ErrUserDoesNotHaveAccessToRepo{UserID: aID, RepoName: repo.Name})
return
}
}
prOpts := &pull_service.NewPullRequestOptions{
Repo: repo,
Issue: prIssue,
LabelIDs: labelIDs,
PullRequest: pr,
AssigneeIDs: assigneeIDs,
}
prOpts.Reviewers, prOpts.TeamReviewers = parseReviewersByNames(ctx, form.Reviewers, form.TeamReviewers)
if ctx.Written() {
return
}
if err := pull_service.NewPullRequest(ctx, prOpts); err != nil {
if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
2025-02-17 14:13:17 +08:00
ctx.APIError(http.StatusBadRequest, err)
2024-03-04 09:16:03 +01:00
} else if errors.Is(err, user_model.ErrBlockedUser) {
2025-02-17 14:13:17 +08:00
ctx.APIError(http.StatusForbidden, err)
} else if errors.Is(err, issues_model.ErrMustCollaborator) {
2025-02-17 14:13:17 +08:00
ctx.APIError(http.StatusForbidden, err)
2024-03-04 09:16:03 +01:00
} else {
2025-02-18 04:41:03 +08:00
ctx.APIErrorInternal(err)
2018-05-09 18:29:04 +02:00
}
2016-12-02 12:10:39 +01:00
return
}
log.Trace("Pull request created: %d/%d", repo.ID, prIssue.ID)
2022-03-22 08:03:22 +01:00
ctx.JSON(http.StatusCreated, convert.ToAPIPullRequest(ctx, pr, ctx.Doer))
2016-12-02 12:10:39 +01:00
}
// EditPullRequest does what it says
2021-01-26 23:36:53 +08:00
func EditPullRequest(ctx *context.APIContext) {
2017-11-12 23:02:25 -08:00
// swagger:operation PATCH /repos/{owner}/{repo}/pulls/{index} repository repoEditPullRequest
// ---
2019-11-03 15:46:32 +01:00
// summary: Update a pull request. If using deadline only the date will be taken into account, and time of day ignored.
2017-11-12 23:02:25 -08: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
// format: int64
2017-11-12 23:02:25 -08:00
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/EditPullRequestOption"
// responses:
// "201":
// "$ref": "#/responses/PullRequest"
2019-12-20 18:07:12 +01:00
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
// "409":
// "$ref": "#/responses/error"
2019-12-20 18:07:12 +01:00
// "412":
// "$ref": "#/responses/error"
// "422":
// "$ref": "#/responses/validationError"
2021-01-26 23:36:53 +08:00
form := web.GetForm(ctx).(*api.EditPullRequestOption)
2024-12-24 21:47:45 +08:00
pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
2016-12-02 12:10:39 +01:00
if err != nil {
if issues_model.IsErrPullRequestNotExist(err) {
2025-02-17 14:13:17 +08:00
ctx.APIErrorNotFound()
2016-12-02 12:10:39 +01:00
} else {
2025-02-18 04:41:03 +08:00
ctx.APIErrorInternal(err)
2016-12-02 12:10:39 +01:00
}
return
}
err = pr.LoadIssue(ctx)
2019-06-12 21:41:28 +02:00
if err != nil {
2025-02-18 04:41:03 +08:00
ctx.APIErrorInternal(err)
2019-06-12 21:41:28 +02:00
return
}
2016-12-02 12:10:39 +01:00
issue := pr.Issue
2018-12-13 23:55:43 +08:00
issue.Repo = ctx.Repo.Repository
2016-12-02 12:10:39 +01:00
if err := issue.LoadAttributes(ctx); err != nil {
2025-02-18 04:41:03 +08:00
ctx.APIErrorInternal(err)
return
}
2022-03-22 08:03:22 +01:00
if !issue.IsPoster(ctx.Doer.ID) && !ctx.Repo.CanWrite(unit.TypePullRequests) {
2019-12-20 18:07:12 +01:00
ctx.Status(http.StatusForbidden)
2016-12-02 12:10:39 +01:00
return
}
// Fail fast: if content_version is provided and already stale, reject
// before any mutations. The DB-level check in ChangeContent still
// handles concurrent requests.
// TODO: wrap all mutations in a transaction to fully prevent partial writes.
if form.ContentVersion != nil && *form.ContentVersion != issue.ContentVersion {
ctx.APIError(http.StatusConflict, issues_model.ErrIssueAlreadyChanged)
return
}
2016-12-02 12:10:39 +01:00
if len(form.Title) > 0 {
err = issue_service.ChangeTitle(ctx, issue, ctx.Doer, form.Title)
if err != nil {
2025-02-18 04:41:03 +08:00
ctx.APIErrorInternal(err)
return
}
2016-12-02 12:10:39 +01:00
}
if form.Body != nil {
contentVersion := issue.ContentVersion
if form.ContentVersion != nil {
contentVersion = *form.ContentVersion
}
err = issue_service.ChangeContent(ctx, issue, ctx.Doer, *form.Body, contentVersion)
if err != nil {
if errors.Is(err, issues_model.ErrIssueAlreadyChanged) {
ctx.APIError(http.StatusConflict, err)
return
}
2025-02-18 04:41:03 +08:00
ctx.APIErrorInternal(err)
return
}
2016-12-02 12:10:39 +01:00
}
2019-11-03 15:46:32 +01: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())
}
2023-09-29 14:12:54 +02:00
if err := issues_model.UpdateIssueDeadline(ctx, issue, deadlineUnix, ctx.Doer); err != nil {
2025-02-18 04:41:03 +08:00
ctx.APIErrorInternal(err)
return
}
issue.DeadlineUnix = deadlineUnix
2018-05-01 21:05:28 +02:00
}
2018-05-09 18:29:04 +02:00
// Add/delete assignees
2016-12-02 12:10:39 +01:00
2019-03-09 16:15:45 -05:00
// Deleting is done the GitHub way (quote from their api documentation):
2018-05-09 18:29:04 +02: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-10 03:57:58 +08:00
if ctx.Repo.CanWrite(unit.TypePullRequests) && (form.Assignees != nil || len(form.Assignee) > 0) {
err = issue_service.UpdateAssignees(ctx, issue, form.Assignee, form.Assignees, ctx.Doer)
2018-05-09 18:29:04 +02:00
if err != nil {
if user_model.IsErrUserNotExist(err) {
2025-02-17 14:13:17 +08:00
ctx.APIError(http.StatusUnprocessableEntity, fmt.Sprintf("Assignee does not exist: [name: %s]", err))
2024-03-04 09:16:03 +01:00
} else if errors.Is(err, user_model.ErrBlockedUser) {
2025-02-17 14:13:17 +08:00
ctx.APIError(http.StatusForbidden, err)
2018-05-09 18:29:04 +02:00
} else {
2025-02-18 04:41:03 +08:00
ctx.APIErrorInternal(err)
2018-05-09 18:29:04 +02:00
}
2016-12-02 12:10:39 +01:00
return
}
}
2018-05-09 18:29:04 +02:00
2021-11-10 03:57:58 +08:00
if ctx.Repo.CanWrite(unit.TypePullRequests) && form.Milestone != 0 &&
2016-12-02 12:10:39 +01:00
issue.MilestoneID != form.Milestone {
oldMilestoneID := issue.MilestoneID
issue.MilestoneID = form.Milestone
issue.Milestone, err = issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, form.Milestone)
if err != nil {
ctx.APIErrorInternal(err)
return
}
if err = issue_service.ChangeMilestoneAssign(ctx, issue, ctx.Doer, oldMilestoneID); err != nil {
2025-02-18 04:41:03 +08:00
ctx.APIErrorInternal(err)
2016-12-02 12:10:39 +01:00
return
}
}
2021-11-10 03:57:58 +08:00
if ctx.Repo.CanWrite(unit.TypePullRequests) && form.Labels != nil {
labels, err := issues_model.GetLabelsInRepoByIDs(ctx, ctx.Repo.Repository.ID, form.Labels)
if err != nil {
2025-02-18 04:41:03 +08:00
ctx.APIErrorInternal(err)
return
}
if ctx.Repo.Owner.IsOrganization() {
orgLabels, err := issues_model.GetLabelsInOrgByIDs(ctx, ctx.Repo.Owner.ID, form.Labels)
if err != nil {
2025-02-18 04:41:03 +08:00
ctx.APIErrorInternal(err)
return
}
labels = append(labels, orgLabels...)
}
if err = issues_model.ReplaceIssueLabels(ctx, issue, labels, ctx.Doer); err != nil {
2025-02-18 04:41:03 +08:00
ctx.APIErrorInternal(err)
return
}
}
2016-12-02 12:10:39 +01:00
if form.State != nil {
if pr.HasMerged {
2025-02-17 14:13:17 +08:00
ctx.APIError(http.StatusPreconditionFailed, "cannot change state of this pull request, it was already merged")
return
}
state := api.StateType(*form.State)
closeOrReopenIssue(ctx, issue, state)
if ctx.Written() {
return
}
2016-12-02 12:10:39 +01:00
}
// change pull target branch
if !pr.HasMerged && len(form.Base) != 0 && form.Base != pr.BaseBranch {
branchExist, err := git_model.IsBranchExist(ctx, ctx.Repo.Repository.ID, form.Base)
if err != nil {
ctx.APIErrorInternal(err)
return
}
if !branchExist {
2025-02-17 14:13:17 +08:00
ctx.APIError(http.StatusNotFound, fmt.Errorf("new base '%s' not exist", form.Base))
return
}
2022-03-22 08:03:22 +01:00
if err := pull_service.ChangeTargetBranch(ctx, pr, ctx.Doer, form.Base); err != nil {
if issues_model.IsErrPullRequestAlreadyExists(err) {
2025-02-17 14:13:17 +08:00
ctx.APIError(http.StatusConflict, err)
return
} else if issues_model.IsErrIssueIsClosed(err) {
2025-02-17 14:13:17 +08:00
ctx.APIError(http.StatusUnprocessableEntity, err)
return
} else if pull_service.IsErrPullRequestHasMerged(err) {
2025-02-17 14:13:17 +08:00
ctx.APIError(http.StatusConflict, err)
return
}
2025-02-17 14:13:17 +08:00
ctx.APIErrorInternal(err)
return
}
notify_service.PullRequestChangeTargetBranch(ctx, ctx.Doer, pr, form.Base)
}
// update allow edits
if form.AllowMaintainerEdit != nil {
if err := pull_service.SetAllowEdits(ctx, ctx.Doer, pr, *form.AllowMaintainerEdit); err != nil {
2024-09-09 22:23:07 -04:00
if errors.Is(err, pull_service.ErrUserHasNoPermissionForAction) {
2025-02-17 14:13:17 +08:00
ctx.APIError(http.StatusForbidden, fmt.Sprintf("SetAllowEdits: %s", err))
return
}
2025-02-17 14:13:17 +08:00
ctx.APIErrorInternal(err)
return
}
}
2016-12-02 12:10:39 +01:00
// Refetch from database
pr, err = issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, pr.Index)
2016-12-02 12:10:39 +01:00
if err != nil {
if issues_model.IsErrPullRequestNotExist(err) {
2025-02-17 14:13:17 +08:00
ctx.APIErrorNotFound()
2016-12-02 12:10:39 +01:00
} else {
2025-02-18 04:41:03 +08:00
ctx.APIErrorInternal(err)
2016-12-02 12:10:39 +01:00
}
return
}
2017-11-12 23:02:25 -08:00
// TODO this should be 200, not 201
2022-03-22 08:03:22 +01:00
ctx.JSON(http.StatusCreated, convert.ToAPIPullRequest(ctx, pr, ctx.Doer))
2016-12-02 12:10:39 +01:00
}
// IsPullRequestMerged checks if a PR exists given an index
func IsPullRequestMerged(ctx *context.APIContext) {
2017-11-12 23:02:25 -08: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
// format: int64
2017-11-12 23:02:25 -08:00
// required: true
// responses:
// "204":
// description: pull request has been merged
// "404":
// description: pull request has not been merged
2019-12-20 18:07:12 +01:00
2024-12-24 21:47:45 +08:00
pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
2016-12-02 12:10:39 +01:00
if err != nil {
if issues_model.IsErrPullRequestNotExist(err) {
2025-02-17 14:13:17 +08:00
ctx.APIErrorNotFound()
2016-12-02 12:10:39 +01:00
} else {
2025-02-18 04:41:03 +08:00
ctx.APIErrorInternal(err)
2016-12-02 12:10:39 +01:00
}
return
}
if pr.HasMerged {
2019-12-20 18:07:12 +01:00
ctx.Status(http.StatusNoContent)
2016-12-02 12:10:39 +01:00
}
2025-02-17 14:13:17 +08:00
ctx.APIErrorNotFound()
2016-12-02 12:10:39 +01:00
}
// MergePullRequest merges a PR given an index
2021-01-26 23:36:53 +08:00
func MergePullRequest(ctx *context.APIContext) {
2017-11-12 23:02:25 -08: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
// format: int64
2017-11-12 23:02:25 -08:00
// required: true
// - name: body
// in: body
// schema:
// $ref: "#/definitions/MergePullRequestOption"
2017-11-12 23:02:25 -08:00
// responses:
// "200":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
2017-11-12 23:02:25 -08:00
// "405":
// "$ref": "#/responses/empty"
2019-12-20 18:07:12 +01:00
// "409":
// "$ref": "#/responses/error"
// "423":
// "$ref": "#/responses/repoArchivedError"
2019-12-20 18:07:12 +01:00
form := web.GetForm(ctx).(*forms.MergePullRequestForm)
2024-12-24 21:47:45 +08:00
pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
2016-12-02 12:10:39 +01:00
if err != nil {
if issues_model.IsErrPullRequestNotExist(err) {
2025-02-17 14:13:17 +08:00
ctx.APIErrorNotFound("GetPullRequestByIndex", err)
2016-12-02 12:10:39 +01:00
} else {
2025-02-18 04:41:03 +08:00
ctx.APIErrorInternal(err)
2016-12-02 12:10:39 +01:00
}
return
}
if err := pr.LoadHeadRepo(ctx); err != nil {
2025-02-18 04:41:03 +08:00
ctx.APIErrorInternal(err)
2016-12-02 12:10:39 +01:00
return
}
if err := pr.LoadIssue(ctx); err != nil {
2025-02-18 04:41:03 +08:00
ctx.APIErrorInternal(err)
2019-06-12 21:41:28 +02:00
return
}
2016-12-02 12:10:39 +01:00
pr.Issue.Repo = ctx.Repo.Repository
if ctx.IsSigned {
// Update issue-user.
if err = activities_model.SetIssueReadBy(ctx, pr.Issue.ID, ctx.Doer.ID); err != nil {
2025-02-18 04:41:03 +08:00
ctx.APIErrorInternal(err)
2016-12-02 12:10:39 +01:00
return
}
}
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
}
// start with merging by checking
2024-05-09 01:11:43 +09:00
if err := pull_service.CheckPullMergeable(ctx, ctx.Doer, &ctx.Repo.Permission, pr, mergeCheckType, form.ForceMerge); err != nil {
if errors.Is(err, pull_service.ErrIsClosed) {
2025-02-17 14:13:17 +08:00
ctx.APIErrorNotFound()
} else if errors.Is(err, pull_service.ErrNoPermissionToMerge) {
2025-02-17 14:13:17 +08:00
ctx.APIError(http.StatusMethodNotAllowed, "User not allowed to merge PR")
} else if errors.Is(err, pull_service.ErrHasMerged) {
ctx.APIError(http.StatusMethodNotAllowed, "The PR is already merged")
} else if errors.Is(err, pull_service.ErrIsWorkInProgress) {
2025-02-17 14:13:17 +08:00
ctx.APIError(http.StatusMethodNotAllowed, "Work in progress PRs cannot be merged")
2024-05-09 01:11:43 +09:00
} else if errors.Is(err, pull_service.ErrNotMergeableState) {
2025-02-17 14:13:17 +08:00
ctx.APIError(http.StatusMethodNotAllowed, "Please try again later")
} else if errors.Is(err, pull_service.ErrNotReadyToMerge) {
2025-02-17 14:13:17 +08:00
ctx.APIError(http.StatusMethodNotAllowed, err)
} else if asymkey_service.IsErrWontSign(err) {
2025-02-17 14:13:17 +08:00
ctx.APIError(http.StatusMethodNotAllowed, err)
} else {
2025-02-17 14:13:17 +08:00
ctx.APIErrorInternal(err)
}
return
}
// handle manually-merged mark
if manuallyMerged {
if err := pull_service.MergedManually(ctx, pr, ctx.Doer, ctx.Repo.GitRepo, form.MergeCommitID); err != nil {
if pull_service.IsErrInvalidMergeStyle(err) {
2025-02-17 14:13:17 +08:00
ctx.APIError(http.StatusMethodNotAllowed, fmt.Errorf("%s is not allowed an allowed merge style for this repository", repo_model.MergeStyle(form.Do)))
return
}
if strings.Contains(err.Error(), "Wrong commit ID") {
ctx.APIError(http.StatusConflict, err)
return
}
2025-02-18 04:41:03 +08:00
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusOK)
return
}
if len(form.Do) == 0 {
form.Do = string(repo_model.MergeStyleMerge)
}
message := strings.TrimSpace(form.MergeTitleField)
if len(message) == 0 {
message, _, err = pull_service.GetDefaultMergeMessage(ctx, ctx.Repo.GitRepo, pr, repo_model.MergeStyle(form.Do))
if err != nil {
2025-02-18 04:41:03 +08:00
ctx.APIErrorInternal(err)
return
}
}
form.MergeMessageField = strings.TrimSpace(form.MergeMessageField)
if len(form.MergeMessageField) > 0 {
message += "\n\n" + form.MergeMessageField
}
deleteBranchAfterMerge, err := pull_service.ShouldDeleteBranchAfterMerge(ctx, form.DeleteBranchAfterMerge, ctx.Repo.Repository, pr)
if err != nil {
ctx.APIErrorInternal(err)
return
}
if form.MergeWhenChecksSucceed {
scheduled, err := automerge.ScheduleAutoMerge(ctx, ctx.Doer, pr, repo_model.MergeStyle(form.Do), message, deleteBranchAfterMerge)
if err != nil {
if pull_model.IsErrAlreadyScheduledToAutoMerge(err) {
2025-02-17 14:13:17 +08:00
ctx.APIError(http.StatusConflict, err)
return
}
2025-02-18 04:41:03 +08:00
ctx.APIErrorInternal(err)
return
} else if scheduled {
// nothing more to do ...
ctx.Status(http.StatusCreated)
return
}
}
if err := pull_service.Merge(ctx, pr, ctx.Doer, repo_model.MergeStyle(form.Do), form.HeadCommitID, message, false); err != nil {
if pull_service.IsErrInvalidMergeStyle(err) {
2025-02-17 14:13:17 +08:00
ctx.APIError(http.StatusMethodNotAllowed, fmt.Errorf("%s is not allowed an allowed merge style for this repository", repo_model.MergeStyle(form.Do)))
} else if pull_service.IsErrMergeConflicts(err) {
conflictError := err.(pull_service.ErrMergeConflicts)
ctx.JSON(http.StatusConflict, conflictError)
} else if pull_service.IsErrRebaseConflicts(err) {
conflictError := err.(pull_service.ErrRebaseConflicts)
ctx.JSON(http.StatusConflict, conflictError)
} else if pull_service.IsErrMergeUnrelatedHistories(err) {
conflictError := err.(pull_service.ErrMergeUnrelatedHistories)
ctx.JSON(http.StatusConflict, conflictError)
} else if git.IsErrPushOutOfDate(err) {
2025-02-17 14:13:17 +08:00
ctx.APIError(http.StatusConflict, "merge push out of date")
} else if pull_service.IsErrSHADoesNotMatch(err) {
2025-02-17 14:13:17 +08:00
ctx.APIError(http.StatusConflict, "head out of date")
} else if git.IsErrPushRejected(err) {
errPushRej := err.(*git.ErrPushRejected)
if len(errPushRej.Message) == 0 {
2025-02-17 14:13:17 +08:00
ctx.APIError(http.StatusConflict, "PushRejected without remote error message")
} else {
2025-02-17 14:13:17 +08:00
ctx.APIError(http.StatusConflict, "PushRejected with remote message: "+errPushRej.Message)
}
} else {
2025-02-18 04:41:03 +08:00
ctx.APIErrorInternal(err)
}
2016-12-02 12:10:39 +01:00
return
}
log.Trace("Pull request merged: %d", pr.ID)
if deleteBranchAfterMerge {
if err = repo_service.DeleteBranchAfterMerge(ctx, ctx.Doer, pr.ID, nil); err != nil {
// no way to tell users that what error happens, and the PR has been merged, so ignore the error
log.Debug("DeleteBranchAfterMerge: pr %d, err: %v", pr.ID, err)
}
}
2019-12-20 18:07:12 +01:00
ctx.Status(http.StatusOK)
2016-12-02 12:10:39 +01:00
}
// parseCompareInfo returns non-nil if it succeeds, it always writes to the context and returns nil if it fails
func parseCompareInfo(ctx *context.APIContext, compareParam string) (result *git_service.CompareInfo, closer func()) {
baseRepo := ctx.Repo.Repository
2026-01-01 10:32:19 -08:00
compareReq := common.ParseCompareRouterParam(compareParam)
2016-12-02 12:10:39 +01:00
// remove the check when we support compare with carets
2026-01-01 10:32:19 -08:00
if compareReq.BaseOriRefSuffix != "" {
ctx.APIError(http.StatusBadRequest, "Unsupported comparison syntax: ref with suffix")
return nil, nil
}
2016-12-02 12:10:39 +01:00
_, headRepo, err := common.GetHeadOwnerAndRepo(ctx, baseRepo, compareReq)
switch {
case errors.Is(err, util.ErrInvalidArgument):
ctx.APIError(http.StatusBadRequest, err.Error())
return nil, nil
case errors.Is(err, util.ErrNotExist):
ctx.APIErrorNotFound()
return nil, nil
case err != nil:
ctx.APIErrorInternal(err)
return nil, nil
2016-12-02 12:10:39 +01:00
}
isSameRepo := baseRepo.ID == headRepo.ID
2016-12-02 12:10:39 +01:00
var headGitRepo *git.Repository
if isSameRepo {
headGitRepo = ctx.Repo.GitRepo
closer = func() {} // no need to close the head repo because it shares the base repo
2016-12-02 12:10:39 +01:00
} else {
headGitRepo, err = gitrepo.OpenRepository(ctx, headRepo)
2016-12-02 12:10:39 +01:00
if err != nil {
2025-02-18 04:41:03 +08:00
ctx.APIErrorInternal(err)
return nil, nil
2016-12-02 12:10:39 +01:00
}
closer = func() { _ = headGitRepo.Close() }
2016-12-02 12:10:39 +01:00
}
defer func() {
if result == nil && !isSameRepo {
_ = headGitRepo.Close()
}
}()
2016-12-02 12:10:39 +01:00
// user should have permission to read baseRepo's codes and pulls, NOT headRepo's
permBase, err := access_model.GetDoerRepoPermission(ctx, baseRepo, ctx.Doer)
if err != nil {
2025-02-18 04:41:03 +08:00
ctx.APIErrorInternal(err)
return nil, nil
}
2025-12-17 13:21:04 -08:00
if !permBase.CanRead(unit.TypeCode) {
log.Trace("Permission Denied: User %-v cannot read code in Repo %-v\nUser in baseRepo has Permissions: %-+v", ctx.Doer, baseRepo, permBase)
ctx.APIErrorNotFound("can't read baseRepo UnitTypeCode")
return nil, nil
}
// user should have permission to read headRepo's codes
// TODO: could the logic be simplified if the headRepo is the same as the baseRepo? Need to think more about it.
permHead, err := access_model.GetDoerRepoPermission(ctx, headRepo, ctx.Doer)
if err != nil {
2025-02-18 04:41:03 +08:00
ctx.APIErrorInternal(err)
return nil, nil
}
2021-11-10 03:57:58 +08:00
if !permHead.CanRead(unit.TypeCode) {
log.Trace("Permission Denied: User: %-v cannot read code in Repo: %-v\nUser in headRepo has Permissions: %-+v", ctx.Doer, headRepo, permHead)
2025-02-17 14:13:17 +08:00
ctx.APIErrorNotFound("Can't read headRepo UnitTypeCode")
return nil, nil
2016-12-02 12:10:39 +01:00
}
baseRef := ctx.Repo.GitRepo.UnstableGuessRefByShortName(util.IfZero(compareReq.BaseOriRef, baseRepo.GetPullRequestTargetBranch(ctx)))
headRef := headGitRepo.UnstableGuessRefByShortName(util.IfZero(compareReq.HeadOriRef, headRepo.DefaultBranch))
log.Trace("Repo path: %q, base ref: %q->%q, head ref: %q->%q", ctx.Repo.Repository.RelativePath(), compareReq.BaseOriRef, baseRef, compareReq.HeadOriRef, headRef)
baseRefValid := baseRef.IsBranch() || baseRef.IsTag() || git.IsStringLikelyCommitID(git.ObjectFormatFromName(ctx.Repo.Repository.ObjectFormatName), baseRef.ShortName())
headRefValid := headRef.IsBranch() || headRef.IsTag() || git.IsStringLikelyCommitID(git.ObjectFormatFromName(headRepo.ObjectFormatName), headRef.ShortName())
// Check if base&head ref are valid.
if !baseRefValid || !headRefValid {
2025-02-17 14:13:17 +08:00
ctx.APIErrorNotFound()
return nil, nil
2016-12-02 12:10:39 +01:00
}
compareInfo, err := git_service.GetCompareInfo(ctx, baseRepo, headRepo, headGitRepo, baseRef, headRef, compareReq.DirectComparison(), false)
2016-12-02 12:10:39 +01:00
if err != nil {
2025-02-18 04:41:03 +08:00
ctx.APIErrorInternal(err)
return nil, nil
2016-12-02 12:10:39 +01:00
}
return compareInfo, closer
2016-12-02 12:10:39 +01:00
}
2020-08-05 04:55:22 +08: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
// - name: style
// in: query
// description: how to update pull request
// type: string
// enum: [merge, rebase]
2020-08-05 04:55:22 +08:00
// responses:
// "200":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
// "409":
// "$ref": "#/responses/error"
// "422":
// "$ref": "#/responses/validationError"
2024-12-24 21:47:45 +08:00
pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
2020-08-05 04:55:22 +08:00
if err != nil {
if issues_model.IsErrPullRequestNotExist(err) {
2025-02-17 14:13:17 +08:00
ctx.APIErrorNotFound()
2020-08-05 04:55:22 +08:00
} else {
2025-02-18 04:41:03 +08:00
ctx.APIErrorInternal(err)
2020-08-05 04:55:22 +08:00
}
return
}
if pr.HasMerged {
2025-02-17 14:13:17 +08:00
ctx.APIError(http.StatusUnprocessableEntity, err)
2020-08-05 04:55:22 +08:00
return
}
if err = pr.LoadIssue(ctx); err != nil {
2025-02-18 04:41:03 +08:00
ctx.APIErrorInternal(err)
2020-08-05 04:55:22 +08:00
return
}
if pr.Issue.IsClosed {
2025-02-17 14:13:17 +08:00
ctx.APIError(http.StatusUnprocessableEntity, err)
2020-08-05 04:55:22 +08:00
return
}
if err = pr.LoadBaseRepo(ctx); err != nil {
2025-02-18 04:41:03 +08:00
ctx.APIErrorInternal(err)
2020-08-05 04:55:22 +08:00
return
}
if err = pr.LoadHeadRepo(ctx); err != nil {
2025-02-18 04:41:03 +08:00
ctx.APIErrorInternal(err)
2020-08-05 04:55:22 +08:00
return
}
rebase := ctx.FormString("style") == "rebase"
2022-04-28 13:48:48 +02:00
allowedUpdateByMerge, allowedUpdateByRebase, err := pull_service.IsUserAllowedToUpdate(ctx, pr, ctx.Doer)
2020-08-05 04:55:22 +08:00
if err != nil {
2025-02-18 04:41:03 +08:00
ctx.APIErrorInternal(err)
2020-08-05 04:55:22 +08:00
return
}
if (!allowedUpdateByMerge && !rebase) || (rebase && !allowedUpdateByRebase) {
2020-08-05 04:55:22 +08:00
ctx.Status(http.StatusForbidden)
return
}
// default merge commit message
message := fmt.Sprintf("Merge branch '%s' into %s", pr.BaseBranch, pr.HeadBranch)
if err = pull_service.Update(graceful.GetManager().ShutdownContext(), pr, ctx.Doer, message, rebase); err != nil {
if pull_service.IsErrMergeConflicts(err) {
2025-02-17 14:13:17 +08:00
ctx.APIError(http.StatusConflict, "merge failed because of conflict")
2020-08-05 04:55:22 +08:00
return
} else if pull_service.IsErrRebaseConflicts(err) {
2025-02-17 14:13:17 +08:00
ctx.APIError(http.StatusConflict, "rebase failed because of conflict")
return
2020-08-05 04:55:22 +08:00
}
2025-02-18 04:41:03 +08:00
ctx.APIErrorInternal(err)
2020-08-05 04:55:22 +08:00
return
}
ctx.Status(http.StatusOK)
}
2021-07-02 14:19:57 +02: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"
// "423":
// "$ref": "#/responses/repoArchivedError"
2024-12-24 21:47:45 +08:00
pullIndex := ctx.PathParamInt64("index")
pull, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, pullIndex)
if err != nil {
if issues_model.IsErrPullRequestNotExist(err) {
2025-02-17 14:13:17 +08:00
ctx.APIErrorNotFound()
return
}
2025-02-17 14:13:17 +08:00
ctx.APIErrorInternal(err)
return
}
exist, autoMerge, err := pull_model.GetScheduledMergeByPullID(ctx, pull.ID)
if err != nil {
2025-02-17 14:13:17 +08:00
ctx.APIErrorInternal(err)
return
}
if !exist {
2025-02-17 14:13:17 +08:00
ctx.APIErrorNotFound()
return
}
if ctx.Doer.ID != autoMerge.DoerID {
2026-01-12 13:47:06 -08:00
allowed, err := pull_service.IsUserAllowedToMerge(ctx, pull, ctx.Repo.Permission, ctx.Doer)
if err != nil {
2025-02-17 14:13:17 +08:00
ctx.APIErrorInternal(err)
return
}
if !allowed {
2025-02-17 14:13:17 +08:00
ctx.APIError(http.StatusForbidden, "user has no permission to cancel the scheduled auto merge")
return
}
}
if err := automerge.RemoveScheduledAutoMerge(ctx, ctx.Doer, pull); err != nil {
2025-02-17 14:13:17 +08:00
ctx.APIErrorInternal(err)
} else {
ctx.Status(http.StatusNoContent)
}
}
2021-07-02 14:19:57 +02: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
// - name: verification
// in: query
// description: include verification for every commit (disable for speedup, default 'true')
// type: boolean
// - name: files
// in: query
// description: include a list of affected files for every commit (disable for speedup, default 'true')
// type: boolean
2021-07-02 14:19:57 +02:00
// responses:
// "200":
// "$ref": "#/responses/CommitList"
// "404":
// "$ref": "#/responses/notFound"
2024-12-24 21:47:45 +08:00
pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
2021-07-02 14:19:57 +02:00
if err != nil {
if issues_model.IsErrPullRequestNotExist(err) {
2025-02-17 14:13:17 +08:00
ctx.APIErrorNotFound()
2021-07-02 14:19:57 +02:00
} else {
2025-02-18 04:41:03 +08:00
ctx.APIErrorInternal(err)
2021-07-02 14:19:57 +02:00
}
return
}
if err := pr.LoadBaseRepo(ctx); err != nil {
2025-02-17 14:13:17 +08:00
ctx.APIErrorInternal(err)
2021-07-02 14:19:57 +02:00
return
}
var compareInfo *git_service.CompareInfo
baseGitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, pr.BaseRepo)
2021-07-02 14:19:57 +02:00
if err != nil {
2025-02-17 14:13:17 +08:00
ctx.APIErrorInternal(err)
2021-07-02 14:19:57 +02:00
return
}
defer closer.Close()
2021-07-02 14:19:57 +02:00
if pr.HasMerged {
compareInfo, err = git_service.GetCompareInfo(ctx, pr.BaseRepo, pr.BaseRepo, baseGitRepo, git.RefName(pr.MergeBase), git.RefName(pr.GetGitHeadRefName()), false, false)
2021-07-02 14:19:57 +02:00
} else {
compareInfo, err = git_service.GetCompareInfo(ctx, pr.BaseRepo, pr.BaseRepo, baseGitRepo, git.RefNameFromBranch(pr.BaseBranch), git.RefName(pr.GetGitHeadRefName()), false, false)
2021-07-02 14:19:57 +02:00
}
if gitcmd.StderrHasPrefix(err, "fatal: bad revision") {
ctx.APIError(http.StatusNotFound, "invalid base branch or revision")
return
} else if err != nil {
2025-02-17 14:13:17 +08:00
ctx.APIErrorInternal(err)
2021-07-02 14:19:57 +02:00
return
}
listOptions := utils.GetListOptions(ctx)
totalNumberOfCommits := len(compareInfo.Commits)
2021-07-02 14:19:57 +02:00
totalNumberOfPages := int(math.Ceil(float64(totalNumberOfCommits) / float64(listOptions.PageSize)))
userCache := make(map[string]*user_model.User)
2021-07-02 14:19:57 +02:00
2024-01-15 10:19:25 +08:00
start, limit := listOptions.GetSkipTake()
2021-07-02 14:19:57 +02:00
2024-01-15 10:19:25 +08:00
limit = min(limit, totalNumberOfCommits-start)
limit = max(limit, 0)
2021-07-02 14:19:57 +02:00
verification := ctx.FormString("verification") == "" || ctx.FormBool("verification")
files := ctx.FormString("files") == "" || ctx.FormBool("files")
2024-01-15 10:19:25 +08:00
apiCommits := make([]*api.Commit, 0, limit)
for i := start; i < start+limit; i++ {
apiCommit, err := convert.ToCommit(ctx, ctx.Repo.Repository, baseGitRepo, compareInfo.Commits[i], userCache,
convert.ToCommitOptions{
Stat: true,
Verification: verification,
Files: files,
})
2021-07-02 14:19:57 +02:00
if err != nil {
2025-02-17 14:13:17 +08:00
ctx.APIErrorInternal(err)
2021-07-02 14:19:57 +02:00
return
}
2021-08-09 20:08:51 +02:00
apiCommits = append(apiCommits, apiCommit)
2021-07-02 14:19:57 +02:00
}
2026-03-08 15:35:50 +01:00
ctx.SetLinkHeader(int64(totalNumberOfCommits), listOptions.PageSize)
2021-08-12 14:43:08 +02:00
ctx.SetTotalCountHeader(int64(totalNumberOfCommits))
2021-07-02 14:19:57 +02:00
2021-12-15 14:59:57 +08: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 14:43:08 +02:00
ctx.AppendAccessControlExposeHeaders("X-Page", "X-PerPage", "X-PageCount", "X-HasMore")
2021-07-02 14:19:57 +02:00
ctx.JSON(http.StatusOK, &apiCommits)
}
// 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"
2024-12-24 21:47:45 +08:00
pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
if issues_model.IsErrPullRequestNotExist(err) {
2025-02-17 14:13:17 +08:00
ctx.APIErrorNotFound()
} else {
2025-02-18 04:41:03 +08:00
ctx.APIErrorInternal(err)
}
return
}
if err := pr.LoadBaseRepo(ctx); err != nil {
2025-02-17 14:13:17 +08:00
ctx.APIErrorInternal(err)
return
}
if err := pr.LoadHeadRepo(ctx); err != nil {
2025-02-17 14:13:17 +08:00
ctx.APIErrorInternal(err)
return
}
baseGitRepo := ctx.Repo.GitRepo
var compareInfo *git_service.CompareInfo
if pr.HasMerged {
compareInfo, err = git_service.GetCompareInfo(ctx, pr.BaseRepo, pr.BaseRepo, baseGitRepo, git.RefName(pr.MergeBase), git.RefName(pr.GetGitHeadRefName()), false, false)
} else {
compareInfo, err = git_service.GetCompareInfo(ctx, pr.BaseRepo, pr.BaseRepo, baseGitRepo, git.RefNameFromBranch(pr.BaseBranch), git.RefName(pr.GetGitHeadRefName()), false, false)
}
if err != nil {
2025-02-17 14:13:17 +08:00
ctx.APIErrorInternal(err)
return
}
headCommitID, err := baseGitRepo.GetRefCommitID(pr.GetGitHeadRefName())
if err != nil {
2025-02-17 14:13:17 +08:00
ctx.APIErrorInternal(err)
return
}
startCommitID := compareInfo.MergeBase
endCommitID := headCommitID
maxLines := setting.Git.MaxGitDiffLines
// FIXME: If there are too many files in the repo, may cause some unpredictable issues.
diff, err := gitdiff.GetDiffForAPI(ctx, baseGitRepo,
&gitdiff.DiffOptions{
BeforeCommitID: startCommitID,
AfterCommitID: endCommitID,
SkipTo: ctx.FormString("skip-to"),
MaxLines: maxLines,
MaxLineCharacters: setting.Git.MaxGitDiffLineCharacters,
MaxFiles: -1, // GetDiff() will return all files
WhitespaceBehavior: gitdiff.GetWhitespaceFlag(ctx.FormString("whitespace")),
})
if err != nil {
2025-02-17 14:13:17 +08:00
ctx.APIErrorInternal(err)
return
}
diffShortStat, err := gitdiff.GetDiffShortStat(ctx, ctx.Repo.Repository, baseGitRepo, startCommitID, endCommitID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
listOptions := utils.GetListOptions(ctx)
totalNumberOfFiles := diffShortStat.NumFiles
totalNumberOfPages := int(math.Ceil(float64(totalNumberOfFiles) / float64(listOptions.PageSize)))
2024-01-15 10:19:25 +08:00
start, limit := listOptions.GetSkipTake()
2024-01-15 10:19:25 +08:00
limit = min(limit, totalNumberOfFiles-start)
2024-01-15 10:19:25 +08:00
limit = max(limit, 0)
2024-01-15 10:19:25 +08:00
apiFiles := make([]*api.ChangedFile, 0, limit)
for i := start; i < start+limit; i++ {
// refs/pull/1/head stores the HEAD commit ID, allowing all related commits to be found in the base repository.
// The head repository might have been deleted, so we should not rely on it here.
apiFiles = append(apiFiles, convert.ToChangedFile(diff.Files[i], pr.BaseRepo, endCommitID))
}
2026-03-08 15:35:50 +01:00
ctx.SetLinkHeader(int64(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)
}