code.gitea.io/gitea@v1.22.3/routers/api/v1/repo/pull.go (about) 1 // Copyright 2016 The Gitea Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package repo 5 6 import ( 7 "errors" 8 "fmt" 9 "math" 10 "net/http" 11 "strconv" 12 "strings" 13 "time" 14 15 "code.gitea.io/gitea/models" 16 activities_model "code.gitea.io/gitea/models/activities" 17 git_model "code.gitea.io/gitea/models/git" 18 issues_model "code.gitea.io/gitea/models/issues" 19 access_model "code.gitea.io/gitea/models/perm/access" 20 pull_model "code.gitea.io/gitea/models/pull" 21 repo_model "code.gitea.io/gitea/models/repo" 22 "code.gitea.io/gitea/models/unit" 23 user_model "code.gitea.io/gitea/models/user" 24 "code.gitea.io/gitea/modules/base" 25 "code.gitea.io/gitea/modules/git" 26 "code.gitea.io/gitea/modules/gitrepo" 27 "code.gitea.io/gitea/modules/log" 28 "code.gitea.io/gitea/modules/setting" 29 api "code.gitea.io/gitea/modules/structs" 30 "code.gitea.io/gitea/modules/timeutil" 31 "code.gitea.io/gitea/modules/web" 32 "code.gitea.io/gitea/routers/api/v1/utils" 33 asymkey_service "code.gitea.io/gitea/services/asymkey" 34 "code.gitea.io/gitea/services/automerge" 35 "code.gitea.io/gitea/services/context" 36 "code.gitea.io/gitea/services/convert" 37 "code.gitea.io/gitea/services/forms" 38 "code.gitea.io/gitea/services/gitdiff" 39 issue_service "code.gitea.io/gitea/services/issue" 40 notify_service "code.gitea.io/gitea/services/notify" 41 pull_service "code.gitea.io/gitea/services/pull" 42 repo_service "code.gitea.io/gitea/services/repository" 43 ) 44 45 // ListPullRequests returns a list of all PRs 46 func ListPullRequests(ctx *context.APIContext) { 47 // swagger:operation GET /repos/{owner}/{repo}/pulls repository repoListPullRequests 48 // --- 49 // summary: List a repo's pull requests 50 // produces: 51 // - application/json 52 // parameters: 53 // - name: owner 54 // in: path 55 // description: owner of the repo 56 // type: string 57 // required: true 58 // - name: repo 59 // in: path 60 // description: name of the repo 61 // type: string 62 // required: true 63 // - name: state 64 // in: query 65 // description: "State of pull request: open or closed (optional)" 66 // type: string 67 // enum: [closed, open, all] 68 // - name: sort 69 // in: query 70 // description: "Type of sort" 71 // type: string 72 // enum: [oldest, recentupdate, leastupdate, mostcomment, leastcomment, priority] 73 // - name: milestone 74 // in: query 75 // description: "ID of the milestone" 76 // type: integer 77 // format: int64 78 // - name: labels 79 // in: query 80 // description: "Label IDs" 81 // type: array 82 // collectionFormat: multi 83 // items: 84 // type: integer 85 // format: int64 86 // - name: page 87 // in: query 88 // description: page number of results to return (1-based) 89 // type: integer 90 // - name: limit 91 // in: query 92 // description: page size of results 93 // type: integer 94 // responses: 95 // "200": 96 // "$ref": "#/responses/PullRequestList" 97 // "404": 98 // "$ref": "#/responses/notFound" 99 100 labelIDs, err := base.StringsToInt64s(ctx.FormStrings("labels")) 101 if err != nil { 102 ctx.Error(http.StatusInternalServerError, "PullRequests", err) 103 return 104 } 105 listOptions := utils.GetListOptions(ctx) 106 prs, maxResults, err := issues_model.PullRequests(ctx, ctx.Repo.Repository.ID, &issues_model.PullRequestsOptions{ 107 ListOptions: listOptions, 108 State: ctx.FormTrim("state"), 109 SortType: ctx.FormTrim("sort"), 110 Labels: labelIDs, 111 MilestoneID: ctx.FormInt64("milestone"), 112 }) 113 if err != nil { 114 ctx.Error(http.StatusInternalServerError, "PullRequests", err) 115 return 116 } 117 118 apiPrs := make([]*api.PullRequest, len(prs)) 119 for i := range prs { 120 if err = prs[i].LoadIssue(ctx); err != nil { 121 ctx.Error(http.StatusInternalServerError, "LoadIssue", err) 122 return 123 } 124 if err = prs[i].LoadAttributes(ctx); err != nil { 125 ctx.Error(http.StatusInternalServerError, "LoadAttributes", err) 126 return 127 } 128 if err = prs[i].LoadBaseRepo(ctx); err != nil { 129 ctx.Error(http.StatusInternalServerError, "LoadBaseRepo", err) 130 return 131 } 132 if err = prs[i].LoadHeadRepo(ctx); err != nil { 133 ctx.Error(http.StatusInternalServerError, "LoadHeadRepo", err) 134 return 135 } 136 apiPrs[i] = convert.ToAPIPullRequest(ctx, prs[i], ctx.Doer) 137 } 138 139 ctx.SetLinkHeader(int(maxResults), listOptions.PageSize) 140 ctx.SetTotalCountHeader(maxResults) 141 ctx.JSON(http.StatusOK, &apiPrs) 142 } 143 144 // GetPullRequest returns a single PR based on index 145 func GetPullRequest(ctx *context.APIContext) { 146 // swagger:operation GET /repos/{owner}/{repo}/pulls/{index} repository repoGetPullRequest 147 // --- 148 // summary: Get a pull request 149 // produces: 150 // - application/json 151 // parameters: 152 // - name: owner 153 // in: path 154 // description: owner of the repo 155 // type: string 156 // required: true 157 // - name: repo 158 // in: path 159 // description: name of the repo 160 // type: string 161 // required: true 162 // - name: index 163 // in: path 164 // description: index of the pull request to get 165 // type: integer 166 // format: int64 167 // required: true 168 // responses: 169 // "200": 170 // "$ref": "#/responses/PullRequest" 171 // "404": 172 // "$ref": "#/responses/notFound" 173 174 pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) 175 if err != nil { 176 if issues_model.IsErrPullRequestNotExist(err) { 177 ctx.NotFound() 178 } else { 179 ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err) 180 } 181 return 182 } 183 184 if err = pr.LoadBaseRepo(ctx); err != nil { 185 ctx.Error(http.StatusInternalServerError, "LoadBaseRepo", err) 186 return 187 } 188 if err = pr.LoadHeadRepo(ctx); err != nil { 189 ctx.Error(http.StatusInternalServerError, "LoadHeadRepo", err) 190 return 191 } 192 ctx.JSON(http.StatusOK, convert.ToAPIPullRequest(ctx, pr, ctx.Doer)) 193 } 194 195 // GetPullRequest returns a single PR based on index 196 func GetPullRequestByBaseHead(ctx *context.APIContext) { 197 // swagger:operation GET /repos/{owner}/{repo}/pulls/{base}/{head} repository repoGetPullRequestByBaseHead 198 // --- 199 // summary: Get a pull request by base and head 200 // produces: 201 // - application/json 202 // parameters: 203 // - name: owner 204 // in: path 205 // description: owner of the repo 206 // type: string 207 // required: true 208 // - name: repo 209 // in: path 210 // description: name of the repo 211 // type: string 212 // required: true 213 // - name: base 214 // in: path 215 // description: base of the pull request to get 216 // type: string 217 // required: true 218 // - name: head 219 // in: path 220 // description: head of the pull request to get 221 // type: string 222 // required: true 223 // responses: 224 // "200": 225 // "$ref": "#/responses/PullRequest" 226 // "404": 227 // "$ref": "#/responses/notFound" 228 229 var headRepoID int64 230 var headBranch string 231 head := ctx.Params("*") 232 if strings.Contains(head, ":") { 233 split := strings.SplitN(head, ":", 2) 234 headBranch = split[1] 235 var owner, name string 236 if strings.Contains(split[0], "/") { 237 split = strings.Split(split[0], "/") 238 owner = split[0] 239 name = split[1] 240 } else { 241 owner = split[0] 242 name = ctx.Repo.Repository.Name 243 } 244 repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, owner, name) 245 if err != nil { 246 if repo_model.IsErrRepoNotExist(err) { 247 ctx.NotFound() 248 } else { 249 ctx.Error(http.StatusInternalServerError, "GetRepositoryByOwnerName", err) 250 } 251 return 252 } 253 headRepoID = repo.ID 254 } else { 255 headRepoID = ctx.Repo.Repository.ID 256 headBranch = head 257 } 258 259 pr, err := issues_model.GetPullRequestByBaseHeadInfo(ctx, ctx.Repo.Repository.ID, headRepoID, ctx.Params(":base"), headBranch) 260 if err != nil { 261 if issues_model.IsErrPullRequestNotExist(err) { 262 ctx.NotFound() 263 } else { 264 ctx.Error(http.StatusInternalServerError, "GetPullRequestByBaseHeadInfo", err) 265 } 266 return 267 } 268 269 if err = pr.LoadBaseRepo(ctx); err != nil { 270 ctx.Error(http.StatusInternalServerError, "LoadBaseRepo", err) 271 return 272 } 273 if err = pr.LoadHeadRepo(ctx); err != nil { 274 ctx.Error(http.StatusInternalServerError, "LoadHeadRepo", err) 275 return 276 } 277 ctx.JSON(http.StatusOK, convert.ToAPIPullRequest(ctx, pr, ctx.Doer)) 278 } 279 280 // DownloadPullDiffOrPatch render a pull's raw diff or patch 281 func DownloadPullDiffOrPatch(ctx *context.APIContext) { 282 // swagger:operation GET /repos/{owner}/{repo}/pulls/{index}.{diffType} repository repoDownloadPullDiffOrPatch 283 // --- 284 // summary: Get a pull request diff or patch 285 // produces: 286 // - text/plain 287 // parameters: 288 // - name: owner 289 // in: path 290 // description: owner of the repo 291 // type: string 292 // required: true 293 // - name: repo 294 // in: path 295 // description: name of the repo 296 // type: string 297 // required: true 298 // - name: index 299 // in: path 300 // description: index of the pull request to get 301 // type: integer 302 // format: int64 303 // required: true 304 // - name: diffType 305 // in: path 306 // description: whether the output is diff or patch 307 // type: string 308 // enum: [diff, patch] 309 // required: true 310 // - name: binary 311 // in: query 312 // description: whether to include binary file changes. if true, the diff is applicable with `git apply` 313 // type: boolean 314 // responses: 315 // "200": 316 // "$ref": "#/responses/string" 317 // "404": 318 // "$ref": "#/responses/notFound" 319 pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) 320 if err != nil { 321 if issues_model.IsErrPullRequestNotExist(err) { 322 ctx.NotFound() 323 } else { 324 ctx.InternalServerError(err) 325 } 326 return 327 } 328 var patch bool 329 if ctx.Params(":diffType") == "diff" { 330 patch = false 331 } else { 332 patch = true 333 } 334 335 binary := ctx.FormBool("binary") 336 337 if err := pull_service.DownloadDiffOrPatch(ctx, pr, ctx, patch, binary); err != nil { 338 ctx.InternalServerError(err) 339 return 340 } 341 } 342 343 // CreatePullRequest does what it says 344 func CreatePullRequest(ctx *context.APIContext) { 345 // swagger:operation POST /repos/{owner}/{repo}/pulls repository repoCreatePullRequest 346 // --- 347 // summary: Create a pull request 348 // consumes: 349 // - application/json 350 // produces: 351 // - application/json 352 // parameters: 353 // - name: owner 354 // in: path 355 // description: owner of the repo 356 // type: string 357 // required: true 358 // - name: repo 359 // in: path 360 // description: name of the repo 361 // type: string 362 // required: true 363 // - name: body 364 // in: body 365 // schema: 366 // "$ref": "#/definitions/CreatePullRequestOption" 367 // responses: 368 // "201": 369 // "$ref": "#/responses/PullRequest" 370 // "403": 371 // "$ref": "#/responses/forbidden" 372 // "404": 373 // "$ref": "#/responses/notFound" 374 // "409": 375 // "$ref": "#/responses/error" 376 // "422": 377 // "$ref": "#/responses/validationError" 378 // "423": 379 // "$ref": "#/responses/repoArchivedError" 380 381 form := *web.GetForm(ctx).(*api.CreatePullRequestOption) 382 if form.Head == form.Base { 383 ctx.Error(http.StatusUnprocessableEntity, "BaseHeadSame", 384 "Invalid PullRequest: There are no changes between the head and the base") 385 return 386 } 387 388 var ( 389 repo = ctx.Repo.Repository 390 labelIDs []int64 391 milestoneID int64 392 ) 393 394 // Get repo/branch information 395 _, headRepo, headGitRepo, compareInfo, baseBranch, headBranch := parseCompareInfo(ctx, form) 396 if ctx.Written() { 397 return 398 } 399 defer headGitRepo.Close() 400 401 // Check if another PR exists with the same targets 402 existingPr, err := issues_model.GetUnmergedPullRequest(ctx, headRepo.ID, ctx.Repo.Repository.ID, headBranch, baseBranch, issues_model.PullRequestFlowGithub) 403 if err != nil { 404 if !issues_model.IsErrPullRequestNotExist(err) { 405 ctx.Error(http.StatusInternalServerError, "GetUnmergedPullRequest", err) 406 return 407 } 408 } else { 409 err = issues_model.ErrPullRequestAlreadyExists{ 410 ID: existingPr.ID, 411 IssueID: existingPr.Index, 412 HeadRepoID: existingPr.HeadRepoID, 413 BaseRepoID: existingPr.BaseRepoID, 414 HeadBranch: existingPr.HeadBranch, 415 BaseBranch: existingPr.BaseBranch, 416 } 417 ctx.Error(http.StatusConflict, "GetUnmergedPullRequest", err) 418 return 419 } 420 421 if len(form.Labels) > 0 { 422 labels, err := issues_model.GetLabelsInRepoByIDs(ctx, ctx.Repo.Repository.ID, form.Labels) 423 if err != nil { 424 ctx.Error(http.StatusInternalServerError, "GetLabelsInRepoByIDs", err) 425 return 426 } 427 428 labelIDs = make([]int64, 0, len(labels)) 429 for _, label := range labels { 430 labelIDs = append(labelIDs, label.ID) 431 } 432 433 if ctx.Repo.Owner.IsOrganization() { 434 orgLabels, err := issues_model.GetLabelsInOrgByIDs(ctx, ctx.Repo.Owner.ID, form.Labels) 435 if err != nil { 436 ctx.Error(http.StatusInternalServerError, "GetLabelsInOrgByIDs", err) 437 return 438 } 439 440 orgLabelIDs := make([]int64, 0, len(orgLabels)) 441 for _, orgLabel := range orgLabels { 442 orgLabelIDs = append(orgLabelIDs, orgLabel.ID) 443 } 444 labelIDs = append(labelIDs, orgLabelIDs...) 445 } 446 } 447 448 if form.Milestone > 0 { 449 milestone, err := issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, form.Milestone) 450 if err != nil { 451 if issues_model.IsErrMilestoneNotExist(err) { 452 ctx.NotFound() 453 } else { 454 ctx.Error(http.StatusInternalServerError, "GetMilestoneByRepoID", err) 455 } 456 return 457 } 458 459 milestoneID = milestone.ID 460 } 461 462 var deadlineUnix timeutil.TimeStamp 463 if form.Deadline != nil { 464 deadlineUnix = timeutil.TimeStamp(form.Deadline.Unix()) 465 } 466 467 prIssue := &issues_model.Issue{ 468 RepoID: repo.ID, 469 Title: form.Title, 470 PosterID: ctx.Doer.ID, 471 Poster: ctx.Doer, 472 MilestoneID: milestoneID, 473 IsPull: true, 474 Content: form.Body, 475 DeadlineUnix: deadlineUnix, 476 } 477 pr := &issues_model.PullRequest{ 478 HeadRepoID: headRepo.ID, 479 BaseRepoID: repo.ID, 480 HeadBranch: headBranch, 481 BaseBranch: baseBranch, 482 HeadRepo: headRepo, 483 BaseRepo: repo, 484 MergeBase: compareInfo.MergeBase, 485 Type: issues_model.PullRequestGitea, 486 } 487 488 // Get all assignee IDs 489 assigneeIDs, err := issues_model.MakeIDsFromAPIAssigneesToAdd(ctx, form.Assignee, form.Assignees) 490 if err != nil { 491 if user_model.IsErrUserNotExist(err) { 492 ctx.Error(http.StatusUnprocessableEntity, "", fmt.Sprintf("Assignee does not exist: [name: %s]", err)) 493 } else { 494 ctx.Error(http.StatusInternalServerError, "AddAssigneeByName", err) 495 } 496 return 497 } 498 // Check if the passed assignees is assignable 499 for _, aID := range assigneeIDs { 500 assignee, err := user_model.GetUserByID(ctx, aID) 501 if err != nil { 502 ctx.Error(http.StatusInternalServerError, "GetUserByID", err) 503 return 504 } 505 506 valid, err := access_model.CanBeAssigned(ctx, assignee, repo, true) 507 if err != nil { 508 ctx.Error(http.StatusInternalServerError, "canBeAssigned", err) 509 return 510 } 511 if !valid { 512 ctx.Error(http.StatusUnprocessableEntity, "canBeAssigned", repo_model.ErrUserDoesNotHaveAccessToRepo{UserID: aID, RepoName: repo.Name}) 513 return 514 } 515 } 516 517 if err := pull_service.NewPullRequest(ctx, repo, prIssue, labelIDs, []string{}, pr, assigneeIDs); err != nil { 518 if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) { 519 ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err) 520 } else if errors.Is(err, user_model.ErrBlockedUser) { 521 ctx.Error(http.StatusForbidden, "BlockedUser", err) 522 } else if errors.Is(err, issues_model.ErrMustCollaborator) { 523 ctx.Error(http.StatusForbidden, "MustCollaborator", err) 524 } else { 525 ctx.Error(http.StatusInternalServerError, "NewPullRequest", err) 526 } 527 return 528 } 529 530 log.Trace("Pull request created: %d/%d", repo.ID, prIssue.ID) 531 ctx.JSON(http.StatusCreated, convert.ToAPIPullRequest(ctx, pr, ctx.Doer)) 532 } 533 534 // EditPullRequest does what it says 535 func EditPullRequest(ctx *context.APIContext) { 536 // swagger:operation PATCH /repos/{owner}/{repo}/pulls/{index} repository repoEditPullRequest 537 // --- 538 // summary: Update a pull request. If using deadline only the date will be taken into account, and time of day ignored. 539 // consumes: 540 // - application/json 541 // produces: 542 // - application/json 543 // parameters: 544 // - name: owner 545 // in: path 546 // description: owner of the repo 547 // type: string 548 // required: true 549 // - name: repo 550 // in: path 551 // description: name of the repo 552 // type: string 553 // required: true 554 // - name: index 555 // in: path 556 // description: index of the pull request to edit 557 // type: integer 558 // format: int64 559 // required: true 560 // - name: body 561 // in: body 562 // schema: 563 // "$ref": "#/definitions/EditPullRequestOption" 564 // responses: 565 // "201": 566 // "$ref": "#/responses/PullRequest" 567 // "403": 568 // "$ref": "#/responses/forbidden" 569 // "404": 570 // "$ref": "#/responses/notFound" 571 // "409": 572 // "$ref": "#/responses/error" 573 // "412": 574 // "$ref": "#/responses/error" 575 // "422": 576 // "$ref": "#/responses/validationError" 577 578 form := web.GetForm(ctx).(*api.EditPullRequestOption) 579 pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) 580 if err != nil { 581 if issues_model.IsErrPullRequestNotExist(err) { 582 ctx.NotFound() 583 } else { 584 ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err) 585 } 586 return 587 } 588 589 err = pr.LoadIssue(ctx) 590 if err != nil { 591 ctx.Error(http.StatusInternalServerError, "LoadIssue", err) 592 return 593 } 594 issue := pr.Issue 595 issue.Repo = ctx.Repo.Repository 596 597 if err := issue.LoadAttributes(ctx); err != nil { 598 ctx.Error(http.StatusInternalServerError, "LoadAttributes", err) 599 return 600 } 601 602 if !issue.IsPoster(ctx.Doer.ID) && !ctx.Repo.CanWrite(unit.TypePullRequests) { 603 ctx.Status(http.StatusForbidden) 604 return 605 } 606 607 if len(form.Title) > 0 { 608 err = issue_service.ChangeTitle(ctx, issue, ctx.Doer, form.Title) 609 if err != nil { 610 ctx.Error(http.StatusInternalServerError, "ChangeTitle", err) 611 return 612 } 613 } 614 if form.Body != nil { 615 err = issue_service.ChangeContent(ctx, issue, ctx.Doer, *form.Body) 616 if err != nil { 617 ctx.Error(http.StatusInternalServerError, "ChangeContent", err) 618 return 619 } 620 } 621 622 // Update or remove deadline if set 623 if form.Deadline != nil || form.RemoveDeadline != nil { 624 var deadlineUnix timeutil.TimeStamp 625 if (form.RemoveDeadline == nil || !*form.RemoveDeadline) && !form.Deadline.IsZero() { 626 deadline := time.Date(form.Deadline.Year(), form.Deadline.Month(), form.Deadline.Day(), 627 23, 59, 59, 0, form.Deadline.Location()) 628 deadlineUnix = timeutil.TimeStamp(deadline.Unix()) 629 } 630 631 if err := issues_model.UpdateIssueDeadline(ctx, issue, deadlineUnix, ctx.Doer); err != nil { 632 ctx.Error(http.StatusInternalServerError, "UpdateIssueDeadline", err) 633 return 634 } 635 issue.DeadlineUnix = deadlineUnix 636 } 637 638 // Add/delete assignees 639 640 // Deleting is done the GitHub way (quote from their api documentation): 641 // https://developer.github.com/v3/issues/#edit-an-issue 642 // "assignees" (array): Logins for Users to assign to this issue. 643 // Pass one or more user logins to replace the set of assignees on this Issue. 644 // Send an empty array ([]) to clear all assignees from the Issue. 645 646 if ctx.Repo.CanWrite(unit.TypePullRequests) && (form.Assignees != nil || len(form.Assignee) > 0) { 647 err = issue_service.UpdateAssignees(ctx, issue, form.Assignee, form.Assignees, ctx.Doer) 648 if err != nil { 649 if user_model.IsErrUserNotExist(err) { 650 ctx.Error(http.StatusUnprocessableEntity, "", fmt.Sprintf("Assignee does not exist: [name: %s]", err)) 651 } else if errors.Is(err, user_model.ErrBlockedUser) { 652 ctx.Error(http.StatusForbidden, "UpdateAssignees", err) 653 } else { 654 ctx.Error(http.StatusInternalServerError, "UpdateAssignees", err) 655 } 656 return 657 } 658 } 659 660 if ctx.Repo.CanWrite(unit.TypePullRequests) && form.Milestone != 0 && 661 issue.MilestoneID != form.Milestone { 662 oldMilestoneID := issue.MilestoneID 663 issue.MilestoneID = form.Milestone 664 if err = issue_service.ChangeMilestoneAssign(ctx, issue, ctx.Doer, oldMilestoneID); err != nil { 665 ctx.Error(http.StatusInternalServerError, "ChangeMilestoneAssign", err) 666 return 667 } 668 } 669 670 if ctx.Repo.CanWrite(unit.TypePullRequests) && form.Labels != nil { 671 labels, err := issues_model.GetLabelsInRepoByIDs(ctx, ctx.Repo.Repository.ID, form.Labels) 672 if err != nil { 673 ctx.Error(http.StatusInternalServerError, "GetLabelsInRepoByIDsError", err) 674 return 675 } 676 677 if ctx.Repo.Owner.IsOrganization() { 678 orgLabels, err := issues_model.GetLabelsInOrgByIDs(ctx, ctx.Repo.Owner.ID, form.Labels) 679 if err != nil { 680 ctx.Error(http.StatusInternalServerError, "GetLabelsInOrgByIDs", err) 681 return 682 } 683 684 labels = append(labels, orgLabels...) 685 } 686 687 if err = issues_model.ReplaceIssueLabels(ctx, issue, labels, ctx.Doer); err != nil { 688 ctx.Error(http.StatusInternalServerError, "ReplaceLabelsError", err) 689 return 690 } 691 } 692 693 if form.State != nil { 694 if pr.HasMerged { 695 ctx.Error(http.StatusPreconditionFailed, "MergedPRState", "cannot change state of this pull request, it was already merged") 696 return 697 } 698 699 var isClosed bool 700 switch state := api.StateType(*form.State); state { 701 case api.StateOpen: 702 isClosed = false 703 case api.StateClosed: 704 isClosed = true 705 default: 706 ctx.Error(http.StatusPreconditionFailed, "UnknownPRStateError", fmt.Sprintf("unknown state: %s", state)) 707 return 708 } 709 710 if issue.IsClosed != isClosed { 711 if err := issue_service.ChangeStatus(ctx, issue, ctx.Doer, "", isClosed); err != nil { 712 if issues_model.IsErrDependenciesLeft(err) { 713 ctx.Error(http.StatusPreconditionFailed, "DependenciesLeft", "cannot close this pull request because it still has open dependencies") 714 return 715 } 716 ctx.Error(http.StatusInternalServerError, "ChangeStatus", err) 717 return 718 } 719 } 720 } 721 722 // change pull target branch 723 if !pr.HasMerged && len(form.Base) != 0 && form.Base != pr.BaseBranch { 724 if !ctx.Repo.GitRepo.IsBranchExist(form.Base) { 725 ctx.Error(http.StatusNotFound, "NewBaseBranchNotExist", fmt.Errorf("new base '%s' not exist", form.Base)) 726 return 727 } 728 if err := pull_service.ChangeTargetBranch(ctx, pr, ctx.Doer, form.Base); err != nil { 729 if issues_model.IsErrPullRequestAlreadyExists(err) { 730 ctx.Error(http.StatusConflict, "IsErrPullRequestAlreadyExists", err) 731 return 732 } else if issues_model.IsErrIssueIsClosed(err) { 733 ctx.Error(http.StatusUnprocessableEntity, "IsErrIssueIsClosed", err) 734 return 735 } else if models.IsErrPullRequestHasMerged(err) { 736 ctx.Error(http.StatusConflict, "IsErrPullRequestHasMerged", err) 737 return 738 } 739 ctx.InternalServerError(err) 740 return 741 } 742 notify_service.PullRequestChangeTargetBranch(ctx, ctx.Doer, pr, form.Base) 743 } 744 745 // update allow edits 746 if form.AllowMaintainerEdit != nil { 747 if err := pull_service.SetAllowEdits(ctx, ctx.Doer, pr, *form.AllowMaintainerEdit); err != nil { 748 if errors.Is(pull_service.ErrUserHasNoPermissionForAction, err) { 749 ctx.Error(http.StatusForbidden, "SetAllowEdits", fmt.Sprintf("SetAllowEdits: %s", err)) 750 return 751 } 752 ctx.ServerError("SetAllowEdits", err) 753 return 754 } 755 } 756 757 // Refetch from database 758 pr, err = issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, pr.Index) 759 if err != nil { 760 if issues_model.IsErrPullRequestNotExist(err) { 761 ctx.NotFound() 762 } else { 763 ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err) 764 } 765 return 766 } 767 768 // TODO this should be 200, not 201 769 ctx.JSON(http.StatusCreated, convert.ToAPIPullRequest(ctx, pr, ctx.Doer)) 770 } 771 772 // IsPullRequestMerged checks if a PR exists given an index 773 func IsPullRequestMerged(ctx *context.APIContext) { 774 // swagger:operation GET /repos/{owner}/{repo}/pulls/{index}/merge repository repoPullRequestIsMerged 775 // --- 776 // summary: Check if a pull request has been merged 777 // produces: 778 // - application/json 779 // parameters: 780 // - name: owner 781 // in: path 782 // description: owner of the repo 783 // type: string 784 // required: true 785 // - name: repo 786 // in: path 787 // description: name of the repo 788 // type: string 789 // required: true 790 // - name: index 791 // in: path 792 // description: index of the pull request 793 // type: integer 794 // format: int64 795 // required: true 796 // responses: 797 // "204": 798 // description: pull request has been merged 799 // "404": 800 // description: pull request has not been merged 801 802 pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) 803 if err != nil { 804 if issues_model.IsErrPullRequestNotExist(err) { 805 ctx.NotFound() 806 } else { 807 ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err) 808 } 809 return 810 } 811 812 if pr.HasMerged { 813 ctx.Status(http.StatusNoContent) 814 } 815 ctx.NotFound() 816 } 817 818 // MergePullRequest merges a PR given an index 819 func MergePullRequest(ctx *context.APIContext) { 820 // swagger:operation POST /repos/{owner}/{repo}/pulls/{index}/merge repository repoMergePullRequest 821 // --- 822 // summary: Merge a pull request 823 // produces: 824 // - application/json 825 // parameters: 826 // - name: owner 827 // in: path 828 // description: owner of the repo 829 // type: string 830 // required: true 831 // - name: repo 832 // in: path 833 // description: name of the repo 834 // type: string 835 // required: true 836 // - name: index 837 // in: path 838 // description: index of the pull request to merge 839 // type: integer 840 // format: int64 841 // required: true 842 // - name: body 843 // in: body 844 // schema: 845 // $ref: "#/definitions/MergePullRequestOption" 846 // responses: 847 // "200": 848 // "$ref": "#/responses/empty" 849 // "404": 850 // "$ref": "#/responses/notFound" 851 // "405": 852 // "$ref": "#/responses/empty" 853 // "409": 854 // "$ref": "#/responses/error" 855 // "423": 856 // "$ref": "#/responses/repoArchivedError" 857 858 form := web.GetForm(ctx).(*forms.MergePullRequestForm) 859 860 pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) 861 if err != nil { 862 if issues_model.IsErrPullRequestNotExist(err) { 863 ctx.NotFound("GetPullRequestByIndex", err) 864 } else { 865 ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err) 866 } 867 return 868 } 869 870 if err := pr.LoadHeadRepo(ctx); err != nil { 871 ctx.Error(http.StatusInternalServerError, "LoadHeadRepo", err) 872 return 873 } 874 875 if err := pr.LoadIssue(ctx); err != nil { 876 ctx.Error(http.StatusInternalServerError, "LoadIssue", err) 877 return 878 } 879 pr.Issue.Repo = ctx.Repo.Repository 880 881 if ctx.IsSigned { 882 // Update issue-user. 883 if err = activities_model.SetIssueReadBy(ctx, pr.Issue.ID, ctx.Doer.ID); err != nil { 884 ctx.Error(http.StatusInternalServerError, "ReadBy", err) 885 return 886 } 887 } 888 889 manuallyMerged := repo_model.MergeStyle(form.Do) == repo_model.MergeStyleManuallyMerged 890 891 mergeCheckType := pull_service.MergeCheckTypeGeneral 892 if form.MergeWhenChecksSucceed { 893 mergeCheckType = pull_service.MergeCheckTypeAuto 894 } 895 if manuallyMerged { 896 mergeCheckType = pull_service.MergeCheckTypeManually 897 } 898 899 // start with merging by checking 900 if err := pull_service.CheckPullMergeable(ctx, ctx.Doer, &ctx.Repo.Permission, pr, mergeCheckType, form.ForceMerge); err != nil { 901 if errors.Is(err, pull_service.ErrIsClosed) { 902 ctx.NotFound() 903 } else if errors.Is(err, pull_service.ErrUserNotAllowedToMerge) { 904 ctx.Error(http.StatusMethodNotAllowed, "Merge", "User not allowed to merge PR") 905 } else if errors.Is(err, pull_service.ErrHasMerged) { 906 ctx.Error(http.StatusMethodNotAllowed, "PR already merged", "") 907 } else if errors.Is(err, pull_service.ErrIsWorkInProgress) { 908 ctx.Error(http.StatusMethodNotAllowed, "PR is a work in progress", "Work in progress PRs cannot be merged") 909 } else if errors.Is(err, pull_service.ErrNotMergeableState) { 910 ctx.Error(http.StatusMethodNotAllowed, "PR not in mergeable state", "Please try again later") 911 } else if models.IsErrDisallowedToMerge(err) { 912 ctx.Error(http.StatusMethodNotAllowed, "PR is not ready to be merged", err) 913 } else if asymkey_service.IsErrWontSign(err) { 914 ctx.Error(http.StatusMethodNotAllowed, fmt.Sprintf("Protected branch %s requires signed commits but this merge would not be signed", pr.BaseBranch), err) 915 } else { 916 ctx.InternalServerError(err) 917 } 918 return 919 } 920 921 // handle manually-merged mark 922 if manuallyMerged { 923 if err := pull_service.MergedManually(ctx, pr, ctx.Doer, ctx.Repo.GitRepo, form.MergeCommitID); err != nil { 924 if models.IsErrInvalidMergeStyle(err) { 925 ctx.Error(http.StatusMethodNotAllowed, "Invalid merge style", fmt.Errorf("%s is not allowed an allowed merge style for this repository", repo_model.MergeStyle(form.Do))) 926 return 927 } 928 if strings.Contains(err.Error(), "Wrong commit ID") { 929 ctx.JSON(http.StatusConflict, err) 930 return 931 } 932 ctx.Error(http.StatusInternalServerError, "Manually-Merged", err) 933 return 934 } 935 ctx.Status(http.StatusOK) 936 return 937 } 938 939 if len(form.Do) == 0 { 940 form.Do = string(repo_model.MergeStyleMerge) 941 } 942 943 message := strings.TrimSpace(form.MergeTitleField) 944 if len(message) == 0 { 945 message, _, err = pull_service.GetDefaultMergeMessage(ctx, ctx.Repo.GitRepo, pr, repo_model.MergeStyle(form.Do)) 946 if err != nil { 947 ctx.Error(http.StatusInternalServerError, "GetDefaultMergeMessage", err) 948 return 949 } 950 } 951 952 form.MergeMessageField = strings.TrimSpace(form.MergeMessageField) 953 if len(form.MergeMessageField) > 0 { 954 message += "\n\n" + form.MergeMessageField 955 } 956 957 if form.MergeWhenChecksSucceed { 958 scheduled, err := automerge.ScheduleAutoMerge(ctx, ctx.Doer, pr, repo_model.MergeStyle(form.Do), message) 959 if err != nil { 960 if pull_model.IsErrAlreadyScheduledToAutoMerge(err) { 961 ctx.Error(http.StatusConflict, "ScheduleAutoMerge", err) 962 return 963 } 964 ctx.Error(http.StatusInternalServerError, "ScheduleAutoMerge", err) 965 return 966 } else if scheduled { 967 // nothing more to do ... 968 ctx.Status(http.StatusCreated) 969 return 970 } 971 } 972 973 if err := pull_service.Merge(ctx, pr, ctx.Doer, ctx.Repo.GitRepo, repo_model.MergeStyle(form.Do), form.HeadCommitID, message, false); err != nil { 974 if models.IsErrInvalidMergeStyle(err) { 975 ctx.Error(http.StatusMethodNotAllowed, "Invalid merge style", fmt.Errorf("%s is not allowed an allowed merge style for this repository", repo_model.MergeStyle(form.Do))) 976 } else if models.IsErrMergeConflicts(err) { 977 conflictError := err.(models.ErrMergeConflicts) 978 ctx.JSON(http.StatusConflict, conflictError) 979 } else if models.IsErrRebaseConflicts(err) { 980 conflictError := err.(models.ErrRebaseConflicts) 981 ctx.JSON(http.StatusConflict, conflictError) 982 } else if models.IsErrMergeUnrelatedHistories(err) { 983 conflictError := err.(models.ErrMergeUnrelatedHistories) 984 ctx.JSON(http.StatusConflict, conflictError) 985 } else if git.IsErrPushOutOfDate(err) { 986 ctx.Error(http.StatusConflict, "Merge", "merge push out of date") 987 } else if models.IsErrSHADoesNotMatch(err) { 988 ctx.Error(http.StatusConflict, "Merge", "head out of date") 989 } else if git.IsErrPushRejected(err) { 990 errPushRej := err.(*git.ErrPushRejected) 991 if len(errPushRej.Message) == 0 { 992 ctx.Error(http.StatusConflict, "Merge", "PushRejected without remote error message") 993 } else { 994 ctx.Error(http.StatusConflict, "Merge", "PushRejected with remote message: "+errPushRej.Message) 995 } 996 } else { 997 ctx.Error(http.StatusInternalServerError, "Merge", err) 998 } 999 return 1000 } 1001 log.Trace("Pull request merged: %d", pr.ID) 1002 1003 if form.DeleteBranchAfterMerge { 1004 // Don't cleanup when there are other PR's that use this branch as head branch. 1005 exist, err := issues_model.HasUnmergedPullRequestsByHeadInfo(ctx, pr.HeadRepoID, pr.HeadBranch) 1006 if err != nil { 1007 ctx.ServerError("HasUnmergedPullRequestsByHeadInfo", err) 1008 return 1009 } 1010 if exist { 1011 ctx.Status(http.StatusOK) 1012 return 1013 } 1014 1015 var headRepo *git.Repository 1016 if ctx.Repo != nil && ctx.Repo.Repository != nil && ctx.Repo.Repository.ID == pr.HeadRepoID && ctx.Repo.GitRepo != nil { 1017 headRepo = ctx.Repo.GitRepo 1018 } else { 1019 headRepo, err = gitrepo.OpenRepository(ctx, pr.HeadRepo) 1020 if err != nil { 1021 ctx.ServerError(fmt.Sprintf("OpenRepository[%s]", pr.HeadRepo.FullName()), err) 1022 return 1023 } 1024 defer headRepo.Close() 1025 } 1026 if err := pull_service.RetargetChildrenOnMerge(ctx, ctx.Doer, pr); err != nil { 1027 ctx.Error(http.StatusInternalServerError, "RetargetChildrenOnMerge", err) 1028 return 1029 } 1030 if err := repo_service.DeleteBranch(ctx, ctx.Doer, pr.HeadRepo, headRepo, pr.HeadBranch); err != nil { 1031 switch { 1032 case git.IsErrBranchNotExist(err): 1033 ctx.NotFound(err) 1034 case errors.Is(err, repo_service.ErrBranchIsDefault): 1035 ctx.Error(http.StatusForbidden, "DefaultBranch", fmt.Errorf("can not delete default branch")) 1036 case errors.Is(err, git_model.ErrBranchIsProtected): 1037 ctx.Error(http.StatusForbidden, "IsProtectedBranch", fmt.Errorf("branch protected")) 1038 default: 1039 ctx.Error(http.StatusInternalServerError, "DeleteBranch", err) 1040 } 1041 return 1042 } 1043 if err := issues_model.AddDeletePRBranchComment(ctx, ctx.Doer, pr.BaseRepo, pr.Issue.ID, pr.HeadBranch); err != nil { 1044 // Do not fail here as branch has already been deleted 1045 log.Error("DeleteBranch: %v", err) 1046 } 1047 } 1048 1049 ctx.Status(http.StatusOK) 1050 } 1051 1052 func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption) (*user_model.User, *repo_model.Repository, *git.Repository, *git.CompareInfo, string, string) { 1053 baseRepo := ctx.Repo.Repository 1054 1055 // Get compared branches information 1056 // format: <base branch>...[<head repo>:]<head branch> 1057 // base<-head: master...head:feature 1058 // same repo: master...feature 1059 1060 // TODO: Validate form first? 1061 1062 baseBranch := form.Base 1063 1064 var ( 1065 headUser *user_model.User 1066 headBranch string 1067 isSameRepo bool 1068 err error 1069 ) 1070 1071 // If there is no head repository, it means pull request between same repository. 1072 headInfos := strings.Split(form.Head, ":") 1073 if len(headInfos) == 1 { 1074 isSameRepo = true 1075 headUser = ctx.Repo.Owner 1076 headBranch = headInfos[0] 1077 } else if len(headInfos) == 2 { 1078 headUser, err = user_model.GetUserByName(ctx, headInfos[0]) 1079 if err != nil { 1080 if user_model.IsErrUserNotExist(err) { 1081 ctx.NotFound("GetUserByName") 1082 } else { 1083 ctx.Error(http.StatusInternalServerError, "GetUserByName", err) 1084 } 1085 return nil, nil, nil, nil, "", "" 1086 } 1087 headBranch = headInfos[1] 1088 // The head repository can also point to the same repo 1089 isSameRepo = ctx.Repo.Owner.ID == headUser.ID 1090 } else { 1091 ctx.NotFound() 1092 return nil, nil, nil, nil, "", "" 1093 } 1094 1095 ctx.Repo.PullRequest.SameRepo = isSameRepo 1096 log.Trace("Repo path: %q, base branch: %q, head branch: %q", ctx.Repo.GitRepo.Path, baseBranch, headBranch) 1097 // Check if base branch is valid. 1098 if !ctx.Repo.GitRepo.IsBranchExist(baseBranch) && !ctx.Repo.GitRepo.IsTagExist(baseBranch) { 1099 ctx.NotFound("BaseNotExist") 1100 return nil, nil, nil, nil, "", "" 1101 } 1102 1103 // Check if current user has fork of repository or in the same repository. 1104 headRepo := repo_model.GetForkedRepo(ctx, headUser.ID, baseRepo.ID) 1105 if headRepo == nil && !isSameRepo { 1106 log.Trace("parseCompareInfo[%d]: does not have fork or in same repository", baseRepo.ID) 1107 ctx.NotFound("GetForkedRepo") 1108 return nil, nil, nil, nil, "", "" 1109 } 1110 1111 var headGitRepo *git.Repository 1112 if isSameRepo { 1113 headRepo = ctx.Repo.Repository 1114 headGitRepo = ctx.Repo.GitRepo 1115 } else { 1116 headGitRepo, err = gitrepo.OpenRepository(ctx, headRepo) 1117 if err != nil { 1118 ctx.Error(http.StatusInternalServerError, "OpenRepository", err) 1119 return nil, nil, nil, nil, "", "" 1120 } 1121 } 1122 1123 // user should have permission to read baseRepo's codes and pulls, NOT headRepo's 1124 permBase, err := access_model.GetUserRepoPermission(ctx, baseRepo, ctx.Doer) 1125 if err != nil { 1126 headGitRepo.Close() 1127 ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err) 1128 return nil, nil, nil, nil, "", "" 1129 } 1130 if !permBase.CanReadIssuesOrPulls(true) || !permBase.CanRead(unit.TypeCode) { 1131 if log.IsTrace() { 1132 log.Trace("Permission Denied: User %-v cannot create/read pull requests or cannot read code in Repo %-v\nUser in baseRepo has Permissions: %-+v", 1133 ctx.Doer, 1134 baseRepo, 1135 permBase) 1136 } 1137 headGitRepo.Close() 1138 ctx.NotFound("Can't read pulls or can't read UnitTypeCode") 1139 return nil, nil, nil, nil, "", "" 1140 } 1141 1142 // user should have permission to read headrepo's codes 1143 permHead, err := access_model.GetUserRepoPermission(ctx, headRepo, ctx.Doer) 1144 if err != nil { 1145 headGitRepo.Close() 1146 ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err) 1147 return nil, nil, nil, nil, "", "" 1148 } 1149 if !permHead.CanRead(unit.TypeCode) { 1150 if log.IsTrace() { 1151 log.Trace("Permission Denied: User: %-v cannot read code in Repo: %-v\nUser in headRepo has Permissions: %-+v", 1152 ctx.Doer, 1153 headRepo, 1154 permHead) 1155 } 1156 headGitRepo.Close() 1157 ctx.NotFound("Can't read headRepo UnitTypeCode") 1158 return nil, nil, nil, nil, "", "" 1159 } 1160 1161 // Check if head branch is valid. 1162 if !headGitRepo.IsBranchExist(headBranch) && !headGitRepo.IsTagExist(headBranch) { 1163 headGitRepo.Close() 1164 ctx.NotFound() 1165 return nil, nil, nil, nil, "", "" 1166 } 1167 1168 compareInfo, err := headGitRepo.GetCompareInfo(repo_model.RepoPath(baseRepo.Owner.Name, baseRepo.Name), baseBranch, headBranch, false, false) 1169 if err != nil { 1170 headGitRepo.Close() 1171 ctx.Error(http.StatusInternalServerError, "GetCompareInfo", err) 1172 return nil, nil, nil, nil, "", "" 1173 } 1174 1175 return headUser, headRepo, headGitRepo, compareInfo, baseBranch, headBranch 1176 } 1177 1178 // UpdatePullRequest merge PR's baseBranch into headBranch 1179 func UpdatePullRequest(ctx *context.APIContext) { 1180 // swagger:operation POST /repos/{owner}/{repo}/pulls/{index}/update repository repoUpdatePullRequest 1181 // --- 1182 // summary: Merge PR's baseBranch into headBranch 1183 // produces: 1184 // - application/json 1185 // parameters: 1186 // - name: owner 1187 // in: path 1188 // description: owner of the repo 1189 // type: string 1190 // required: true 1191 // - name: repo 1192 // in: path 1193 // description: name of the repo 1194 // type: string 1195 // required: true 1196 // - name: index 1197 // in: path 1198 // description: index of the pull request to get 1199 // type: integer 1200 // format: int64 1201 // required: true 1202 // - name: style 1203 // in: query 1204 // description: how to update pull request 1205 // type: string 1206 // enum: [merge, rebase] 1207 // responses: 1208 // "200": 1209 // "$ref": "#/responses/empty" 1210 // "403": 1211 // "$ref": "#/responses/forbidden" 1212 // "404": 1213 // "$ref": "#/responses/notFound" 1214 // "409": 1215 // "$ref": "#/responses/error" 1216 // "422": 1217 // "$ref": "#/responses/validationError" 1218 1219 pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) 1220 if err != nil { 1221 if issues_model.IsErrPullRequestNotExist(err) { 1222 ctx.NotFound() 1223 } else { 1224 ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err) 1225 } 1226 return 1227 } 1228 1229 if pr.HasMerged { 1230 ctx.Error(http.StatusUnprocessableEntity, "UpdatePullRequest", err) 1231 return 1232 } 1233 1234 if err = pr.LoadIssue(ctx); err != nil { 1235 ctx.Error(http.StatusInternalServerError, "LoadIssue", err) 1236 return 1237 } 1238 1239 if pr.Issue.IsClosed { 1240 ctx.Error(http.StatusUnprocessableEntity, "UpdatePullRequest", err) 1241 return 1242 } 1243 1244 if err = pr.LoadBaseRepo(ctx); err != nil { 1245 ctx.Error(http.StatusInternalServerError, "LoadBaseRepo", err) 1246 return 1247 } 1248 if err = pr.LoadHeadRepo(ctx); err != nil { 1249 ctx.Error(http.StatusInternalServerError, "LoadHeadRepo", err) 1250 return 1251 } 1252 1253 rebase := ctx.FormString("style") == "rebase" 1254 1255 allowedUpdateByMerge, allowedUpdateByRebase, err := pull_service.IsUserAllowedToUpdate(ctx, pr, ctx.Doer) 1256 if err != nil { 1257 ctx.Error(http.StatusInternalServerError, "IsUserAllowedToMerge", err) 1258 return 1259 } 1260 1261 if (!allowedUpdateByMerge && !rebase) || (rebase && !allowedUpdateByRebase) { 1262 ctx.Status(http.StatusForbidden) 1263 return 1264 } 1265 1266 // default merge commit message 1267 message := fmt.Sprintf("Merge branch '%s' into %s", pr.BaseBranch, pr.HeadBranch) 1268 1269 if err = pull_service.Update(ctx, pr, ctx.Doer, message, rebase); err != nil { 1270 if models.IsErrMergeConflicts(err) { 1271 ctx.Error(http.StatusConflict, "Update", "merge failed because of conflict") 1272 return 1273 } else if models.IsErrRebaseConflicts(err) { 1274 ctx.Error(http.StatusConflict, "Update", "rebase failed because of conflict") 1275 return 1276 } 1277 ctx.Error(http.StatusInternalServerError, "pull_service.Update", err) 1278 return 1279 } 1280 1281 ctx.Status(http.StatusOK) 1282 } 1283 1284 // MergePullRequest cancel an auto merge scheduled for a given PullRequest by index 1285 func CancelScheduledAutoMerge(ctx *context.APIContext) { 1286 // swagger:operation DELETE /repos/{owner}/{repo}/pulls/{index}/merge repository repoCancelScheduledAutoMerge 1287 // --- 1288 // summary: Cancel the scheduled auto merge for the given pull request 1289 // produces: 1290 // - application/json 1291 // parameters: 1292 // - name: owner 1293 // in: path 1294 // description: owner of the repo 1295 // type: string 1296 // required: true 1297 // - name: repo 1298 // in: path 1299 // description: name of the repo 1300 // type: string 1301 // required: true 1302 // - name: index 1303 // in: path 1304 // description: index of the pull request to merge 1305 // type: integer 1306 // format: int64 1307 // required: true 1308 // responses: 1309 // "204": 1310 // "$ref": "#/responses/empty" 1311 // "403": 1312 // "$ref": "#/responses/forbidden" 1313 // "404": 1314 // "$ref": "#/responses/notFound" 1315 // "423": 1316 // "$ref": "#/responses/repoArchivedError" 1317 1318 pullIndex := ctx.ParamsInt64(":index") 1319 pull, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, pullIndex) 1320 if err != nil { 1321 if issues_model.IsErrPullRequestNotExist(err) { 1322 ctx.NotFound() 1323 return 1324 } 1325 ctx.InternalServerError(err) 1326 return 1327 } 1328 1329 exist, autoMerge, err := pull_model.GetScheduledMergeByPullID(ctx, pull.ID) 1330 if err != nil { 1331 ctx.InternalServerError(err) 1332 return 1333 } 1334 if !exist { 1335 ctx.NotFound() 1336 return 1337 } 1338 1339 if ctx.Doer.ID != autoMerge.DoerID { 1340 allowed, err := access_model.IsUserRepoAdmin(ctx, ctx.Repo.Repository, ctx.Doer) 1341 if err != nil { 1342 ctx.InternalServerError(err) 1343 return 1344 } 1345 if !allowed { 1346 ctx.Error(http.StatusForbidden, "No permission to cancel", "user has no permission to cancel the scheduled auto merge") 1347 return 1348 } 1349 } 1350 1351 if err := automerge.RemoveScheduledAutoMerge(ctx, ctx.Doer, pull); err != nil { 1352 ctx.InternalServerError(err) 1353 } else { 1354 ctx.Status(http.StatusNoContent) 1355 } 1356 } 1357 1358 // GetPullRequestCommits gets all commits associated with a given PR 1359 func GetPullRequestCommits(ctx *context.APIContext) { 1360 // swagger:operation GET /repos/{owner}/{repo}/pulls/{index}/commits repository repoGetPullRequestCommits 1361 // --- 1362 // summary: Get commits for a pull request 1363 // produces: 1364 // - application/json 1365 // parameters: 1366 // - name: owner 1367 // in: path 1368 // description: owner of the repo 1369 // type: string 1370 // required: true 1371 // - name: repo 1372 // in: path 1373 // description: name of the repo 1374 // type: string 1375 // required: true 1376 // - name: index 1377 // in: path 1378 // description: index of the pull request to get 1379 // type: integer 1380 // format: int64 1381 // required: true 1382 // - name: page 1383 // in: query 1384 // description: page number of results to return (1-based) 1385 // type: integer 1386 // - name: limit 1387 // in: query 1388 // description: page size of results 1389 // type: integer 1390 // - name: verification 1391 // in: query 1392 // description: include verification for every commit (disable for speedup, default 'true') 1393 // type: boolean 1394 // - name: files 1395 // in: query 1396 // description: include a list of affected files for every commit (disable for speedup, default 'true') 1397 // type: boolean 1398 // responses: 1399 // "200": 1400 // "$ref": "#/responses/CommitList" 1401 // "404": 1402 // "$ref": "#/responses/notFound" 1403 1404 pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) 1405 if err != nil { 1406 if issues_model.IsErrPullRequestNotExist(err) { 1407 ctx.NotFound() 1408 } else { 1409 ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err) 1410 } 1411 return 1412 } 1413 1414 if err := pr.LoadBaseRepo(ctx); err != nil { 1415 ctx.InternalServerError(err) 1416 return 1417 } 1418 1419 var prInfo *git.CompareInfo 1420 baseGitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, pr.BaseRepo) 1421 if err != nil { 1422 ctx.ServerError("OpenRepository", err) 1423 return 1424 } 1425 defer closer.Close() 1426 1427 if pr.HasMerged { 1428 prInfo, err = baseGitRepo.GetCompareInfo(pr.BaseRepo.RepoPath(), pr.MergeBase, pr.GetGitRefName(), false, false) 1429 } else { 1430 prInfo, err = baseGitRepo.GetCompareInfo(pr.BaseRepo.RepoPath(), pr.BaseBranch, pr.GetGitRefName(), false, false) 1431 } 1432 if err != nil { 1433 ctx.ServerError("GetCompareInfo", err) 1434 return 1435 } 1436 commits := prInfo.Commits 1437 1438 listOptions := utils.GetListOptions(ctx) 1439 1440 totalNumberOfCommits := len(commits) 1441 totalNumberOfPages := int(math.Ceil(float64(totalNumberOfCommits) / float64(listOptions.PageSize))) 1442 1443 userCache := make(map[string]*user_model.User) 1444 1445 start, limit := listOptions.GetSkipTake() 1446 1447 limit = min(limit, totalNumberOfCommits-start) 1448 limit = max(limit, 0) 1449 1450 verification := ctx.FormString("verification") == "" || ctx.FormBool("verification") 1451 files := ctx.FormString("files") == "" || ctx.FormBool("files") 1452 1453 apiCommits := make([]*api.Commit, 0, limit) 1454 for i := start; i < start+limit; i++ { 1455 apiCommit, err := convert.ToCommit(ctx, ctx.Repo.Repository, baseGitRepo, commits[i], userCache, 1456 convert.ToCommitOptions{ 1457 Stat: true, 1458 Verification: verification, 1459 Files: files, 1460 }) 1461 if err != nil { 1462 ctx.ServerError("toCommit", err) 1463 return 1464 } 1465 apiCommits = append(apiCommits, apiCommit) 1466 } 1467 1468 ctx.SetLinkHeader(totalNumberOfCommits, listOptions.PageSize) 1469 ctx.SetTotalCountHeader(int64(totalNumberOfCommits)) 1470 1471 ctx.RespHeader().Set("X-Page", strconv.Itoa(listOptions.Page)) 1472 ctx.RespHeader().Set("X-PerPage", strconv.Itoa(listOptions.PageSize)) 1473 ctx.RespHeader().Set("X-PageCount", strconv.Itoa(totalNumberOfPages)) 1474 ctx.RespHeader().Set("X-HasMore", strconv.FormatBool(listOptions.Page < totalNumberOfPages)) 1475 ctx.AppendAccessControlExposeHeaders("X-Page", "X-PerPage", "X-PageCount", "X-HasMore") 1476 1477 ctx.JSON(http.StatusOK, &apiCommits) 1478 } 1479 1480 // GetPullRequestFiles gets all changed files associated with a given PR 1481 func GetPullRequestFiles(ctx *context.APIContext) { 1482 // swagger:operation GET /repos/{owner}/{repo}/pulls/{index}/files repository repoGetPullRequestFiles 1483 // --- 1484 // summary: Get changed files for a pull request 1485 // produces: 1486 // - application/json 1487 // parameters: 1488 // - name: owner 1489 // in: path 1490 // description: owner of the repo 1491 // type: string 1492 // required: true 1493 // - name: repo 1494 // in: path 1495 // description: name of the repo 1496 // type: string 1497 // required: true 1498 // - name: index 1499 // in: path 1500 // description: index of the pull request to get 1501 // type: integer 1502 // format: int64 1503 // required: true 1504 // - name: skip-to 1505 // in: query 1506 // description: skip to given file 1507 // type: string 1508 // - name: whitespace 1509 // in: query 1510 // description: whitespace behavior 1511 // type: string 1512 // enum: [ignore-all, ignore-change, ignore-eol, show-all] 1513 // - name: page 1514 // in: query 1515 // description: page number of results to return (1-based) 1516 // type: integer 1517 // - name: limit 1518 // in: query 1519 // description: page size of results 1520 // type: integer 1521 // responses: 1522 // "200": 1523 // "$ref": "#/responses/ChangedFileList" 1524 // "404": 1525 // "$ref": "#/responses/notFound" 1526 1527 pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) 1528 if err != nil { 1529 if issues_model.IsErrPullRequestNotExist(err) { 1530 ctx.NotFound() 1531 } else { 1532 ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err) 1533 } 1534 return 1535 } 1536 1537 if err := pr.LoadBaseRepo(ctx); err != nil { 1538 ctx.InternalServerError(err) 1539 return 1540 } 1541 1542 if err := pr.LoadHeadRepo(ctx); err != nil { 1543 ctx.InternalServerError(err) 1544 return 1545 } 1546 1547 baseGitRepo := ctx.Repo.GitRepo 1548 1549 var prInfo *git.CompareInfo 1550 if pr.HasMerged { 1551 prInfo, err = baseGitRepo.GetCompareInfo(pr.BaseRepo.RepoPath(), pr.MergeBase, pr.GetGitRefName(), true, false) 1552 } else { 1553 prInfo, err = baseGitRepo.GetCompareInfo(pr.BaseRepo.RepoPath(), pr.BaseBranch, pr.GetGitRefName(), true, false) 1554 } 1555 if err != nil { 1556 ctx.ServerError("GetCompareInfo", err) 1557 return 1558 } 1559 1560 headCommitID, err := baseGitRepo.GetRefCommitID(pr.GetGitRefName()) 1561 if err != nil { 1562 ctx.ServerError("GetRefCommitID", err) 1563 return 1564 } 1565 1566 startCommitID := prInfo.MergeBase 1567 endCommitID := headCommitID 1568 1569 maxLines := setting.Git.MaxGitDiffLines 1570 1571 // FIXME: If there are too many files in the repo, may cause some unpredictable issues. 1572 diff, err := gitdiff.GetDiff(ctx, baseGitRepo, 1573 &gitdiff.DiffOptions{ 1574 BeforeCommitID: startCommitID, 1575 AfterCommitID: endCommitID, 1576 SkipTo: ctx.FormString("skip-to"), 1577 MaxLines: maxLines, 1578 MaxLineCharacters: setting.Git.MaxGitDiffLineCharacters, 1579 MaxFiles: -1, // GetDiff() will return all files 1580 WhitespaceBehavior: gitdiff.GetWhitespaceFlag(ctx.FormString("whitespace")), 1581 }) 1582 if err != nil { 1583 ctx.ServerError("GetDiff", err) 1584 return 1585 } 1586 1587 listOptions := utils.GetListOptions(ctx) 1588 1589 totalNumberOfFiles := diff.NumFiles 1590 totalNumberOfPages := int(math.Ceil(float64(totalNumberOfFiles) / float64(listOptions.PageSize))) 1591 1592 start, limit := listOptions.GetSkipTake() 1593 1594 limit = min(limit, totalNumberOfFiles-start) 1595 1596 limit = max(limit, 0) 1597 1598 apiFiles := make([]*api.ChangedFile, 0, limit) 1599 for i := start; i < start+limit; i++ { 1600 apiFiles = append(apiFiles, convert.ToChangedFile(diff.Files[i], pr.HeadRepo, endCommitID)) 1601 } 1602 1603 ctx.SetLinkHeader(totalNumberOfFiles, listOptions.PageSize) 1604 ctx.SetTotalCountHeader(int64(totalNumberOfFiles)) 1605 1606 ctx.RespHeader().Set("X-Page", strconv.Itoa(listOptions.Page)) 1607 ctx.RespHeader().Set("X-PerPage", strconv.Itoa(listOptions.PageSize)) 1608 ctx.RespHeader().Set("X-PageCount", strconv.Itoa(totalNumberOfPages)) 1609 ctx.RespHeader().Set("X-HasMore", strconv.FormatBool(listOptions.Page < totalNumberOfPages)) 1610 ctx.AppendAccessControlExposeHeaders("X-Page", "X-PerPage", "X-PageCount", "X-HasMore") 1611 1612 ctx.JSON(http.StatusOK, &apiFiles) 1613 }