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