mirror of
https://github.com/go-gitea/gitea.git
synced 2026-08-03 16:04:26 +00:00
9dc04289aa
before: gitrepo vs git packages after: git package fully handle all git operations by the way, use `WithRepo(repo)` instead of `WithDir(repo.Path)` to hide path details. benefits: 1. remove all unnecessary wrappers, developers no need to struggle with "which package should be used" 2. simplify code, RepositoryFacade can (will) be used everywhere, all "path" details are (will be) hidden
1633 lines
47 KiB
Go
1633 lines
47 KiB
Go
// Copyright 2016 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package repo
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
activities_model "gitea.dev/models/activities"
|
|
git_model "gitea.dev/models/git"
|
|
issues_model "gitea.dev/models/issues"
|
|
access_model "gitea.dev/models/perm/access"
|
|
pull_model "gitea.dev/models/pull"
|
|
repo_model "gitea.dev/models/repo"
|
|
"gitea.dev/models/unit"
|
|
user_model "gitea.dev/models/user"
|
|
"gitea.dev/modules/base"
|
|
"gitea.dev/modules/git"
|
|
"gitea.dev/modules/git/gitcmd"
|
|
"gitea.dev/modules/graceful"
|
|
"gitea.dev/modules/log"
|
|
"gitea.dev/modules/optional"
|
|
"gitea.dev/modules/setting"
|
|
api "gitea.dev/modules/structs"
|
|
"gitea.dev/modules/timeutil"
|
|
"gitea.dev/modules/util"
|
|
"gitea.dev/modules/web"
|
|
"gitea.dev/routers/api/v1/utils"
|
|
"gitea.dev/routers/common"
|
|
asymkey_service "gitea.dev/services/asymkey"
|
|
"gitea.dev/services/automerge"
|
|
"gitea.dev/services/context"
|
|
"gitea.dev/services/convert"
|
|
"gitea.dev/services/forms"
|
|
git_service "gitea.dev/services/git"
|
|
"gitea.dev/services/gitdiff"
|
|
issue_service "gitea.dev/services/issue"
|
|
notify_service "gitea.dev/services/notify"
|
|
pull_service "gitea.dev/services/pull"
|
|
repo_service "gitea.dev/services/repository"
|
|
)
|
|
|
|
// ListPullRequests returns a list of all PRs
|
|
func ListPullRequests(ctx *context.APIContext) {
|
|
// 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
|
|
// - 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
|
|
// - name: page
|
|
// in: query
|
|
// description: Page number of results to return (1-based)
|
|
// type: integer
|
|
// minimum: 1
|
|
// default: 1
|
|
// - name: limit
|
|
// in: query
|
|
// description: Page size of results
|
|
// type: integer
|
|
// minimum: 0
|
|
// responses:
|
|
// "200":
|
|
// "$ref": "#/responses/PullRequestList"
|
|
// "404":
|
|
// "$ref": "#/responses/notFound"
|
|
// "500":
|
|
// "$ref": "#/responses/error"
|
|
|
|
labelIDs, err := base.StringsToInt64s(ctx.FormStrings("labels"))
|
|
if err != nil {
|
|
ctx.APIErrorInternal(err)
|
|
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) {
|
|
ctx.APIError(http.StatusBadRequest, err.Error())
|
|
} else {
|
|
ctx.APIErrorInternal(err)
|
|
}
|
|
return
|
|
}
|
|
posterID = poster.ID
|
|
}
|
|
listOptions := utils.GetListOptions(ctx)
|
|
prs, maxResults, err := issues_model.PullRequests(ctx, ctx.Repo.Repository.ID, &issues_model.PullRequestsOptions{
|
|
ListOptions: listOptions,
|
|
State: ctx.FormTrim("state"),
|
|
SortType: ctx.FormTrim("sort"),
|
|
Labels: labelIDs,
|
|
MilestoneID: ctx.FormInt64("milestone"),
|
|
PosterID: posterID,
|
|
BaseBranch: ctx.FormTrim("base_branch"),
|
|
})
|
|
if err != nil {
|
|
ctx.APIErrorInternal(err)
|
|
return
|
|
}
|
|
|
|
apiPrs, err := convert.ToAPIPullRequests(ctx, ctx.Repo.Repository, prs, ctx.Doer)
|
|
if err != nil {
|
|
ctx.APIErrorInternal(err)
|
|
return
|
|
}
|
|
|
|
ctx.SetLinkHeader(maxResults, listOptions.PageSize)
|
|
ctx.SetTotalCountHeader(maxResults)
|
|
ctx.JSON(http.StatusOK, &apiPrs)
|
|
}
|
|
|
|
// GetPullRequest returns a single PR based on index
|
|
func GetPullRequest(ctx *context.APIContext) {
|
|
// 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
|
|
// required: true
|
|
// responses:
|
|
// "200":
|
|
// "$ref": "#/responses/PullRequest"
|
|
// "404":
|
|
// "$ref": "#/responses/notFound"
|
|
|
|
pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
|
|
if err != nil {
|
|
if issues_model.IsErrPullRequestNotExist(err) {
|
|
ctx.APIErrorNotFound()
|
|
} else {
|
|
ctx.APIErrorInternal(err)
|
|
}
|
|
return
|
|
}
|
|
|
|
if err = pr.LoadBaseRepo(ctx); err != nil {
|
|
ctx.APIErrorInternal(err)
|
|
return
|
|
}
|
|
if err = pr.LoadHeadRepo(ctx); err != nil {
|
|
ctx.APIErrorInternal(err)
|
|
return
|
|
}
|
|
|
|
// Consider API access a view for delayed checking.
|
|
pull_service.StartPullRequestCheckOnView(ctx, pr)
|
|
|
|
ctx.JSON(http.StatusOK, convert.ToAPIPullRequest(ctx, pr, ctx.Doer))
|
|
}
|
|
|
|
// 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
|
|
head := ctx.PathParam("*")
|
|
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) {
|
|
ctx.APIErrorNotFound()
|
|
} else {
|
|
ctx.APIErrorInternal(err)
|
|
}
|
|
return
|
|
}
|
|
headRepoID = repo.ID
|
|
} else {
|
|
headRepoID = ctx.Repo.Repository.ID
|
|
headBranch = head
|
|
}
|
|
|
|
pr, err := issues_model.GetPullRequestByBaseHeadInfo(ctx, ctx.Repo.Repository.ID, headRepoID, ctx.PathParam("base"), headBranch)
|
|
if err != nil {
|
|
if issues_model.IsErrPullRequestNotExist(err) {
|
|
ctx.APIErrorNotFound()
|
|
} else {
|
|
ctx.APIErrorInternal(err)
|
|
}
|
|
return
|
|
}
|
|
|
|
if err = pr.LoadBaseRepo(ctx); err != nil {
|
|
ctx.APIErrorInternal(err)
|
|
return
|
|
}
|
|
if err = pr.LoadHeadRepo(ctx); err != nil {
|
|
ctx.APIErrorInternal(err)
|
|
return
|
|
}
|
|
|
|
// Consider API access a view for delayed checking.
|
|
pull_service.StartPullRequestCheckOnView(ctx, pr)
|
|
|
|
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"
|
|
pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
|
|
if err != nil {
|
|
if issues_model.IsErrPullRequestNotExist(err) {
|
|
ctx.APIErrorNotFound()
|
|
} else {
|
|
ctx.APIErrorInternal(err)
|
|
}
|
|
return
|
|
}
|
|
var patch bool
|
|
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 {
|
|
ctx.APIErrorInternal(err)
|
|
return
|
|
}
|
|
}
|
|
|
|
// CreatePullRequest does what it says
|
|
func CreatePullRequest(ctx *context.APIContext) {
|
|
// 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"
|
|
// "403":
|
|
// "$ref": "#/responses/forbidden"
|
|
// "404":
|
|
// "$ref": "#/responses/notFound"
|
|
// "409":
|
|
// "$ref": "#/responses/error"
|
|
// "422":
|
|
// "$ref": "#/responses/validationError"
|
|
// "423":
|
|
// "$ref": "#/responses/repoArchivedError"
|
|
|
|
form := *web.GetForm(ctx).(*api.CreatePullRequestOption)
|
|
if form.Head == form.Base {
|
|
ctx.APIError(http.StatusUnprocessableEntity, "Invalid PullRequest: There are no changes between the head and the base")
|
|
return
|
|
}
|
|
|
|
var (
|
|
repo = ctx.Repo.Repository
|
|
labelIDs []int64
|
|
milestoneID int64
|
|
)
|
|
|
|
// Get repo/branch information
|
|
compareResult, closer := parseCompareInfo(ctx, form.Base+".."+form.Head)
|
|
if ctx.Written() {
|
|
return
|
|
}
|
|
defer closer()
|
|
|
|
if !compareResult.BaseRef.IsBranch() || !compareResult.HeadRef.IsBranch() {
|
|
ctx.APIError(http.StatusUnprocessableEntity, "Invalid PullRequest: base and head must be branches")
|
|
return
|
|
}
|
|
|
|
// 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,
|
|
)
|
|
if err != nil {
|
|
if !issues_model.IsErrPullRequestNotExist(err) {
|
|
ctx.APIErrorInternal(err)
|
|
return
|
|
}
|
|
} else {
|
|
err = issues_model.ErrPullRequestAlreadyExists{
|
|
ID: existingPr.ID,
|
|
IssueID: existingPr.Index,
|
|
HeadRepoID: existingPr.HeadRepoID,
|
|
BaseRepoID: existingPr.BaseRepoID,
|
|
HeadBranch: existingPr.HeadBranch,
|
|
BaseBranch: existingPr.BaseBranch,
|
|
}
|
|
ctx.APIError(http.StatusConflict, err.Error())
|
|
return
|
|
}
|
|
|
|
if len(form.Labels) > 0 {
|
|
labels, err := issues_model.GetLabelsInRepoByIDs(ctx, ctx.Repo.Repository.ID, form.Labels)
|
|
if err != nil {
|
|
ctx.APIErrorInternal(err)
|
|
return
|
|
}
|
|
|
|
labelIDs = make([]int64, 0, len(labels))
|
|
for _, label := range labels {
|
|
labelIDs = append(labelIDs, label.ID)
|
|
}
|
|
|
|
if ctx.Repo.Owner.IsOrganization() {
|
|
orgLabels, err := issues_model.GetLabelsInOrgByIDs(ctx, ctx.Repo.Owner.ID, form.Labels)
|
|
if err != nil {
|
|
ctx.APIErrorInternal(err)
|
|
return
|
|
}
|
|
|
|
orgLabelIDs := make([]int64, 0, len(orgLabels))
|
|
for _, orgLabel := range orgLabels {
|
|
orgLabelIDs = append(orgLabelIDs, orgLabel.ID)
|
|
}
|
|
labelIDs = append(labelIDs, orgLabelIDs...)
|
|
}
|
|
}
|
|
|
|
if form.Milestone > 0 {
|
|
milestone, err := issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, form.Milestone)
|
|
if err != nil {
|
|
if issues_model.IsErrMilestoneNotExist(err) {
|
|
ctx.APIErrorNotFound()
|
|
} else {
|
|
ctx.APIErrorInternal(fmt.Errorf("GetMilestoneByRepoID: %w", err))
|
|
}
|
|
return
|
|
}
|
|
|
|
milestoneID = milestone.ID
|
|
}
|
|
|
|
var deadlineUnix timeutil.TimeStamp
|
|
if form.Deadline != nil {
|
|
deadlineUnix = timeutil.TimeStamp(form.Deadline.Unix())
|
|
}
|
|
|
|
unitPullRequest, err := ctx.Repo.Repository.GetUnit(ctx, unit.TypePullRequests)
|
|
if err != nil {
|
|
ctx.APIErrorInternal(err)
|
|
return
|
|
}
|
|
|
|
prIssue := &issues_model.Issue{
|
|
RepoID: repo.ID,
|
|
Title: form.Title,
|
|
PosterID: ctx.Doer.ID,
|
|
Poster: ctx.Doer,
|
|
MilestoneID: milestoneID,
|
|
IsPull: true,
|
|
Content: form.Body,
|
|
DeadlineUnix: deadlineUnix,
|
|
}
|
|
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.CompareBase,
|
|
Type: issues_model.PullRequestGitea,
|
|
}
|
|
|
|
pr.AllowMaintainerEdit = optional.FromPtr(form.AllowMaintainerEdit).ValueOrDefault(unitPullRequest.PullRequestsConfig().DefaultAllowMaintainerEdit)
|
|
|
|
// Get all assignee IDs
|
|
assigneeIDs, err := issues_model.MakeIDsFromAPIAssigneesToAdd(ctx, form.Assignee, form.Assignees)
|
|
if err != nil {
|
|
if user_model.IsErrUserNotExist(err) {
|
|
ctx.APIError(http.StatusUnprocessableEntity, fmt.Sprintf("Assignee does not exist: [name: %s]", err))
|
|
} else {
|
|
ctx.APIErrorInternal(err)
|
|
}
|
|
return
|
|
}
|
|
// Check if the passed assignees is assignable
|
|
for _, aID := range assigneeIDs {
|
|
assignee, err := user_model.GetUserByID(ctx, aID)
|
|
if err != nil {
|
|
ctx.APIErrorInternal(err)
|
|
return
|
|
}
|
|
|
|
valid, err := access_model.CanBeAssigned(ctx, assignee, repo)
|
|
if err != nil {
|
|
ctx.APIErrorInternal(err)
|
|
return
|
|
}
|
|
if !valid {
|
|
ctx.APIError(http.StatusUnprocessableEntity, repo_model.ErrUserDoesNotHaveAccessToRepo{UserID: aID, RepoName: repo.Name}.Error())
|
|
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) {
|
|
ctx.APIError(http.StatusBadRequest, err.Error())
|
|
} else if errors.Is(err, user_model.ErrBlockedUser) {
|
|
ctx.APIError(http.StatusForbidden, err.Error())
|
|
} else if errors.Is(err, issues_model.ErrMustCollaborator) {
|
|
ctx.APIError(http.StatusForbidden, err.Error())
|
|
} else {
|
|
ctx.APIErrorInternal(err)
|
|
}
|
|
return
|
|
}
|
|
|
|
log.Trace("Pull request created: %d/%d", repo.ID, prIssue.ID)
|
|
ctx.JSON(http.StatusCreated, convert.ToAPIPullRequest(ctx, pr, ctx.Doer))
|
|
}
|
|
|
|
// EditPullRequest does what it says
|
|
func EditPullRequest(ctx *context.APIContext) {
|
|
// swagger:operation PATCH /repos/{owner}/{repo}/pulls/{index} repository repoEditPullRequest
|
|
// ---
|
|
// summary: Update a pull request. If using deadline only the date will be taken into account, and time of day ignored.
|
|
// 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
|
|
// required: true
|
|
// - name: body
|
|
// in: body
|
|
// schema:
|
|
// "$ref": "#/definitions/EditPullRequestOption"
|
|
// responses:
|
|
// "201":
|
|
// "$ref": "#/responses/PullRequest"
|
|
// "403":
|
|
// "$ref": "#/responses/forbidden"
|
|
// "404":
|
|
// "$ref": "#/responses/notFound"
|
|
// "409":
|
|
// "$ref": "#/responses/error"
|
|
// "412":
|
|
// "$ref": "#/responses/error"
|
|
// "422":
|
|
// "$ref": "#/responses/validationError"
|
|
|
|
form := web.GetForm(ctx).(*api.EditPullRequestOption)
|
|
pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
|
|
if err != nil {
|
|
if issues_model.IsErrPullRequestNotExist(err) {
|
|
ctx.APIErrorNotFound()
|
|
} else {
|
|
ctx.APIErrorInternal(err)
|
|
}
|
|
return
|
|
}
|
|
|
|
err = pr.LoadIssue(ctx)
|
|
if err != nil {
|
|
ctx.APIErrorInternal(err)
|
|
return
|
|
}
|
|
issue := pr.Issue
|
|
issue.Repo = ctx.Repo.Repository
|
|
|
|
if err := issue.LoadAttributes(ctx); err != nil {
|
|
ctx.APIErrorInternal(err)
|
|
return
|
|
}
|
|
|
|
if !issue.IsPoster(ctx.Doer.ID) && !ctx.Repo.Permission.CanWrite(unit.TypePullRequests) {
|
|
ctx.Status(http.StatusForbidden)
|
|
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.Error())
|
|
return
|
|
}
|
|
|
|
if len(form.Title) > 0 {
|
|
err = issue_service.ChangeTitle(ctx, issue, ctx.Doer, form.Title)
|
|
if err != nil {
|
|
ctx.APIErrorInternal(err)
|
|
return
|
|
}
|
|
}
|
|
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.Error())
|
|
return
|
|
}
|
|
|
|
ctx.APIErrorInternal(err)
|
|
return
|
|
}
|
|
}
|
|
|
|
// 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())
|
|
}
|
|
|
|
if err := issues_model.UpdateIssueDeadline(ctx, issue, deadlineUnix, ctx.Doer); err != nil {
|
|
ctx.APIErrorInternal(err)
|
|
return
|
|
}
|
|
issue.DeadlineUnix = deadlineUnix
|
|
}
|
|
|
|
// Add/delete assignees
|
|
|
|
// Deleting is done the GitHub way (quote from their api documentation):
|
|
// https://developer.github.com/v3/issues/#edit-an-issue
|
|
// "assignees" (array): Logins for Users to assign to this issue.
|
|
// Pass one or more user logins to replace the set of assignees on this Issue.
|
|
// Send an empty array ([]) to clear all assignees from the Issue.
|
|
|
|
if ctx.Repo.Permission.CanWrite(unit.TypePullRequests) && (form.Assignees != nil || len(form.Assignee) > 0) {
|
|
err = issue_service.UpdateAssignees(ctx, issue, form.Assignee, form.Assignees, ctx.Doer)
|
|
if err != nil {
|
|
if user_model.IsErrUserNotExist(err) {
|
|
ctx.APIError(http.StatusUnprocessableEntity, fmt.Sprintf("Assignee does not exist: [name: %s]", err))
|
|
} else if errors.Is(err, user_model.ErrBlockedUser) {
|
|
ctx.APIError(http.StatusForbidden, err.Error())
|
|
} else {
|
|
ctx.APIErrorInternal(err)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
|
|
if ctx.Repo.Permission.CanWrite(unit.TypePullRequests) && form.Milestone != 0 &&
|
|
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 {
|
|
ctx.APIErrorInternal(err)
|
|
return
|
|
}
|
|
}
|
|
|
|
if ctx.Repo.Permission.CanWrite(unit.TypePullRequests) && form.Labels != nil {
|
|
labels, err := issues_model.GetLabelsInRepoByIDs(ctx, ctx.Repo.Repository.ID, form.Labels)
|
|
if err != nil {
|
|
ctx.APIErrorInternal(err)
|
|
return
|
|
}
|
|
|
|
if ctx.Repo.Owner.IsOrganization() {
|
|
orgLabels, err := issues_model.GetLabelsInOrgByIDs(ctx, ctx.Repo.Owner.ID, form.Labels)
|
|
if err != nil {
|
|
ctx.APIErrorInternal(err)
|
|
return
|
|
}
|
|
|
|
labels = append(labels, orgLabels...)
|
|
}
|
|
|
|
if err = issues_model.ReplaceIssueLabels(ctx, issue, labels, ctx.Doer); err != nil {
|
|
ctx.APIErrorInternal(err)
|
|
return
|
|
}
|
|
}
|
|
|
|
if form.State != nil {
|
|
if pr.HasMerged {
|
|
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
|
|
}
|
|
}
|
|
|
|
// 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 {
|
|
ctx.APIError(http.StatusNotFound, fmt.Sprintf("new base '%s' not exist", form.Base))
|
|
return
|
|
}
|
|
if err := pull_service.ChangeTargetBranch(ctx, pr, ctx.Doer, form.Base); err != nil {
|
|
if issues_model.IsErrPullRequestAlreadyExists(err) {
|
|
ctx.APIError(http.StatusConflict, err.Error())
|
|
return
|
|
} else if issues_model.IsErrIssueIsClosed(err) {
|
|
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
|
|
return
|
|
} else if pull_service.IsErrPullRequestHasMerged(err) {
|
|
ctx.APIError(http.StatusConflict, err.Error())
|
|
return
|
|
}
|
|
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 {
|
|
if errors.Is(err, pull_service.ErrUserHasNoPermissionForAction) {
|
|
ctx.APIError(http.StatusForbidden, fmt.Sprintf("SetAllowEdits: %s", err))
|
|
return
|
|
}
|
|
ctx.APIErrorInternal(err)
|
|
return
|
|
}
|
|
}
|
|
|
|
// Refetch from database
|
|
pr, err = issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, pr.Index)
|
|
if err != nil {
|
|
if issues_model.IsErrPullRequestNotExist(err) {
|
|
ctx.APIErrorNotFound()
|
|
} else {
|
|
ctx.APIErrorInternal(err)
|
|
}
|
|
return
|
|
}
|
|
|
|
// TODO this should be 200, not 201
|
|
ctx.JSON(http.StatusCreated, convert.ToAPIPullRequest(ctx, pr, ctx.Doer))
|
|
}
|
|
|
|
// IsPullRequestMerged checks if a PR exists given an index
|
|
func IsPullRequestMerged(ctx *context.APIContext) {
|
|
// 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
|
|
// required: true
|
|
// responses:
|
|
// "204":
|
|
// description: pull request has been merged
|
|
// "404":
|
|
// description: pull request has not been merged
|
|
|
|
pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
|
|
if err != nil {
|
|
if issues_model.IsErrPullRequestNotExist(err) {
|
|
ctx.APIErrorNotFound()
|
|
} else {
|
|
ctx.APIErrorInternal(err)
|
|
}
|
|
return
|
|
}
|
|
|
|
if pr.HasMerged {
|
|
ctx.Status(http.StatusNoContent)
|
|
}
|
|
ctx.APIErrorNotFound()
|
|
}
|
|
|
|
// MergePullRequest merges a PR given an index
|
|
func MergePullRequest(ctx *context.APIContext) {
|
|
// 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
|
|
// required: true
|
|
// - name: body
|
|
// in: body
|
|
// schema:
|
|
// $ref: "#/definitions/MergePullRequestOption"
|
|
// responses:
|
|
// "200":
|
|
// "$ref": "#/responses/empty"
|
|
// "403":
|
|
// "$ref": "#/responses/forbidden"
|
|
// "404":
|
|
// "$ref": "#/responses/notFound"
|
|
// "405":
|
|
// "$ref": "#/responses/empty"
|
|
// "409":
|
|
// "$ref": "#/responses/error"
|
|
// "423":
|
|
// "$ref": "#/responses/repoArchivedError"
|
|
|
|
form := web.GetForm(ctx).(*forms.MergePullRequestForm)
|
|
|
|
pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
|
|
if err != nil {
|
|
ctx.APIErrorAuto(err)
|
|
return
|
|
}
|
|
|
|
if err := pr.LoadHeadRepo(ctx); err != nil {
|
|
ctx.APIErrorInternal(err)
|
|
return
|
|
}
|
|
|
|
if err := pr.LoadIssue(ctx); err != nil {
|
|
ctx.APIErrorInternal(err)
|
|
return
|
|
}
|
|
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 {
|
|
ctx.APIErrorInternal(err)
|
|
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
|
|
if err := pull_service.CheckPullMergeable(ctx, ctx.Doer, &ctx.Repo.Permission, pr, mergeCheckType, repo_model.MergeStyle(form.Do), form.ForceMerge); err != nil {
|
|
if errors.Is(err, pull_service.ErrIsClosed) {
|
|
ctx.APIErrorNotFound()
|
|
} else if errors.Is(err, pull_service.ErrNoPermissionToMerge) {
|
|
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) {
|
|
ctx.APIError(http.StatusMethodNotAllowed, "Work in progress PRs cannot be merged")
|
|
} else if errors.Is(err, pull_service.ErrNotMergeableState) {
|
|
ctx.APIError(http.StatusMethodNotAllowed, "Please try again later")
|
|
} else if errors.Is(err, pull_service.ErrNotReadyToMerge) {
|
|
ctx.APIError(http.StatusMethodNotAllowed, err.Error())
|
|
} else if asymkey_service.IsErrWontSign(err) {
|
|
ctx.APIError(http.StatusMethodNotAllowed, err.Error())
|
|
} else if errors.Is(err, pull_service.ErrHeadCommitsNotAllVerified) {
|
|
ctx.APIError(http.StatusMethodNotAllowed, err.Error())
|
|
} else {
|
|
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) {
|
|
ctx.APIError(http.StatusMethodNotAllowed, fmt.Sprintf("%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.Error())
|
|
return
|
|
}
|
|
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 {
|
|
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) {
|
|
ctx.APIError(http.StatusConflict, err.Error())
|
|
return
|
|
}
|
|
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) {
|
|
ctx.APIError(http.StatusMethodNotAllowed, fmt.Sprintf("%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) {
|
|
ctx.APIError(http.StatusConflict, "merge push out of date")
|
|
} else if pull_service.IsErrSHADoesNotMatch(err) {
|
|
ctx.APIError(http.StatusConflict, "head out of date")
|
|
} else if git.IsErrPushRejected(err) {
|
|
errPushRej := err.(*git.ErrPushRejected)
|
|
if len(errPushRej.Message) == 0 {
|
|
ctx.APIError(http.StatusConflict, "PushRejected without remote error message")
|
|
} else {
|
|
ctx.APIError(http.StatusConflict, "PushRejected with remote message: "+errPushRej.Message)
|
|
}
|
|
} else {
|
|
ctx.APIErrorInternal(err)
|
|
}
|
|
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)
|
|
}
|
|
}
|
|
|
|
ctx.Status(http.StatusOK)
|
|
}
|
|
|
|
// 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
|
|
compareReq := common.ParseCompareRouterParam(compareParam)
|
|
|
|
_, 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
|
|
}
|
|
|
|
isSameRepo := baseRepo.ID == headRepo.ID
|
|
|
|
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
|
|
} else {
|
|
headGitRepo, err = git.OpenRepository(headRepo)
|
|
if err != nil {
|
|
ctx.APIErrorInternal(err)
|
|
return nil, nil
|
|
}
|
|
closer = func() { _ = headGitRepo.Close() }
|
|
}
|
|
defer func() {
|
|
if result == nil && !isSameRepo {
|
|
_ = headGitRepo.Close()
|
|
}
|
|
}()
|
|
|
|
// 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 {
|
|
ctx.APIErrorInternal(err)
|
|
return nil, nil
|
|
}
|
|
|
|
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 {
|
|
ctx.APIErrorInternal(err)
|
|
return nil, nil
|
|
}
|
|
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)
|
|
ctx.APIErrorNotFound("Can't read headRepo UnitTypeCode")
|
|
return nil, nil
|
|
}
|
|
|
|
baseRef, err := common.ResolveRefWithSuffix(ctx, ctx.Repo.GitRepo, util.IfZero(compareReq.BaseOriRef, baseRepo.GetPullRequestTargetBranch(ctx)), compareReq.BaseOriRefSuffix)
|
|
if err != nil {
|
|
ctx.APIErrorAuto(err)
|
|
return nil, nil
|
|
}
|
|
headRef, err := common.ResolveRefWithSuffix(ctx, headGitRepo, util.IfZero(compareReq.HeadOriRef, headRepo.DefaultBranch), compareReq.HeadOriRefSuffix)
|
|
if err != nil {
|
|
ctx.APIErrorAuto(err)
|
|
return nil, nil
|
|
}
|
|
|
|
log.Trace("Repo: %q, base ref: %q->%q, head ref: %q->%q", ctx.Repo.Repository.FullName(), compareReq.BaseOriRef+compareReq.BaseOriRefSuffix, baseRef, compareReq.HeadOriRef+compareReq.HeadOriRefSuffix, 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 {
|
|
ctx.APIErrorNotFound()
|
|
return nil, nil
|
|
}
|
|
|
|
compareInfo, err := git_service.GetCompareInfo(ctx, baseRepo, headRepo, headGitRepo, baseRef, headRef, compareReq.DirectComparison(), false)
|
|
if err != nil {
|
|
ctx.APIErrorInternal(err)
|
|
return nil, nil
|
|
}
|
|
|
|
return &compareInfo, closer
|
|
}
|
|
|
|
// 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]
|
|
// responses:
|
|
// "200":
|
|
// "$ref": "#/responses/empty"
|
|
// "403":
|
|
// "$ref": "#/responses/forbidden"
|
|
// "404":
|
|
// "$ref": "#/responses/notFound"
|
|
// "409":
|
|
// "$ref": "#/responses/error"
|
|
// "422":
|
|
// "$ref": "#/responses/validationError"
|
|
|
|
pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
|
|
if err != nil {
|
|
if issues_model.IsErrPullRequestNotExist(err) {
|
|
ctx.APIErrorNotFound()
|
|
} else {
|
|
ctx.APIErrorInternal(err)
|
|
}
|
|
return
|
|
}
|
|
|
|
if pr.HasMerged {
|
|
ctx.APIError(http.StatusUnprocessableEntity, "pull request is already merged")
|
|
return
|
|
}
|
|
|
|
if err = pr.LoadIssue(ctx); err != nil {
|
|
ctx.APIErrorInternal(err)
|
|
return
|
|
}
|
|
|
|
if pr.Issue.IsClosed {
|
|
ctx.APIError(http.StatusUnprocessableEntity, "pull request is already closed")
|
|
return
|
|
}
|
|
|
|
if err = pr.LoadBaseRepo(ctx); err != nil {
|
|
ctx.APIErrorInternal(err)
|
|
return
|
|
}
|
|
if err = pr.LoadHeadRepo(ctx); err != nil {
|
|
ctx.APIErrorInternal(err)
|
|
return
|
|
}
|
|
|
|
// a public-only token must not update (push into) a private head repo,
|
|
// even when the base repo named in the route is public
|
|
if !ctx.TokenCanAccessRepo(pr.HeadRepo) {
|
|
ctx.APIErrorNotFound()
|
|
return
|
|
}
|
|
|
|
// keep API back-compat: when no style is given, default to "merge" rather than the repo's DefaultUpdateStyle,
|
|
// so existing API clients keep getting a merge update.
|
|
rebase := repo_model.UpdateStyle(ctx.FormString("style", string(repo_model.UpdateStyleMerge))) == repo_model.UpdateStyleRebase
|
|
|
|
userUpdateStyles, err := pull_service.CheckUserAllowedToUpdate(ctx, pr, ctx.Doer)
|
|
if err != nil {
|
|
ctx.APIErrorInternal(err)
|
|
return
|
|
}
|
|
|
|
if (rebase && !userUpdateStyles.RebaseAllowed) || (!rebase && !userUpdateStyles.MergeAllowed) {
|
|
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) {
|
|
ctx.APIError(http.StatusConflict, "merge failed because of conflict")
|
|
return
|
|
} else if pull_service.IsErrRebaseConflicts(err) {
|
|
ctx.APIError(http.StatusConflict, "rebase failed because of conflict")
|
|
return
|
|
}
|
|
ctx.APIErrorInternal(err)
|
|
return
|
|
}
|
|
|
|
ctx.Status(http.StatusOK)
|
|
}
|
|
|
|
// 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"
|
|
|
|
pullIndex := ctx.PathParamInt64("index")
|
|
pull, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, pullIndex)
|
|
if err != nil {
|
|
if issues_model.IsErrPullRequestNotExist(err) {
|
|
ctx.APIErrorNotFound()
|
|
return
|
|
}
|
|
ctx.APIErrorInternal(err)
|
|
return
|
|
}
|
|
|
|
exist, autoMerge, err := pull_model.GetScheduledMergeByPullID(ctx, pull.ID)
|
|
if err != nil {
|
|
ctx.APIErrorInternal(err)
|
|
return
|
|
}
|
|
if !exist {
|
|
ctx.APIErrorNotFound()
|
|
return
|
|
}
|
|
|
|
if ctx.Doer.ID != autoMerge.DoerID {
|
|
allowed, err := pull_service.IsUserAllowedToMerge(ctx, pull, ctx.Repo.Permission, ctx.Doer)
|
|
if err != nil {
|
|
ctx.APIErrorInternal(err)
|
|
return
|
|
}
|
|
if !allowed {
|
|
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 {
|
|
ctx.APIErrorInternal(err)
|
|
} else {
|
|
ctx.Status(http.StatusNoContent)
|
|
}
|
|
}
|
|
|
|
// 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
|
|
// responses:
|
|
// "200":
|
|
// "$ref": "#/responses/CommitList"
|
|
// "404":
|
|
// "$ref": "#/responses/notFound"
|
|
|
|
pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
|
|
if err != nil {
|
|
if issues_model.IsErrPullRequestNotExist(err) {
|
|
ctx.APIErrorNotFound()
|
|
} else {
|
|
ctx.APIErrorInternal(err)
|
|
}
|
|
return
|
|
}
|
|
|
|
if err := pr.LoadBaseRepo(ctx); err != nil {
|
|
ctx.APIErrorInternal(err)
|
|
return
|
|
}
|
|
|
|
baseGitRepo, closer, err := git.RepositoryFromContextOrOpen(ctx, pr.BaseRepo)
|
|
if err != nil {
|
|
ctx.APIErrorInternal(err)
|
|
return
|
|
}
|
|
defer closer.Close()
|
|
|
|
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 gitcmd.IsStderr(err, gitcmd.StderrBadRevision) {
|
|
ctx.APIError(http.StatusNotFound, "invalid base branch or revision")
|
|
return
|
|
} else if err != nil {
|
|
ctx.APIErrorInternal(err)
|
|
return
|
|
}
|
|
|
|
listOptions := utils.GetListOptions(ctx)
|
|
|
|
totalNumberOfCommits := len(compareInfo.Commits)
|
|
totalNumberOfPages := int(math.Ceil(float64(totalNumberOfCommits) / float64(listOptions.PageSize)))
|
|
|
|
userCache := make(map[string]*user_model.User)
|
|
|
|
start, limit := listOptions.GetSkipTake()
|
|
|
|
limit = min(limit, totalNumberOfCommits-start)
|
|
limit = max(limit, 0)
|
|
|
|
verification := ctx.FormString("verification") == "" || ctx.FormBool("verification")
|
|
files := ctx.FormString("files") == "" || ctx.FormBool("files")
|
|
|
|
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,
|
|
})
|
|
if err != nil {
|
|
ctx.APIErrorInternal(err)
|
|
return
|
|
}
|
|
apiCommits = append(apiCommits, apiCommit)
|
|
}
|
|
|
|
ctx.SetLinkHeader(int64(totalNumberOfCommits), listOptions.PageSize)
|
|
ctx.SetTotalCountHeader(int64(totalNumberOfCommits))
|
|
|
|
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, &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"
|
|
|
|
pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
|
|
if err != nil {
|
|
if issues_model.IsErrPullRequestNotExist(err) {
|
|
ctx.APIErrorNotFound()
|
|
} else {
|
|
ctx.APIErrorInternal(err)
|
|
}
|
|
return
|
|
}
|
|
|
|
if err := pr.LoadBaseRepo(ctx); err != nil {
|
|
ctx.APIErrorInternal(err)
|
|
return
|
|
}
|
|
|
|
if err := pr.LoadHeadRepo(ctx); err != nil {
|
|
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 {
|
|
ctx.APIErrorInternal(err)
|
|
return
|
|
}
|
|
|
|
headCommitID, err := baseGitRepo.GetRefCommitID(ctx, pr.GetGitHeadRefName())
|
|
if err != nil {
|
|
ctx.APIErrorInternal(err)
|
|
return
|
|
}
|
|
|
|
startCommitID := compareInfo.CompareBase
|
|
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 {
|
|
ctx.APIErrorInternal(err)
|
|
return
|
|
}
|
|
|
|
diffShortStat, err := gitdiff.GetDiffShortStat(ctx, 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)))
|
|
|
|
start, limit := listOptions.GetSkipTake()
|
|
|
|
limit = min(limit, totalNumberOfFiles-start)
|
|
|
|
limit = max(limit, 0)
|
|
|
|
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))
|
|
}
|
|
|
|
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)
|
|
}
|