code.gitea.io/gitea@v1.21.7/routers/web/repo/pull.go (about) 1 // Copyright 2018 The Gitea Authors. 2 // Copyright 2014 The Gogs Authors. 3 // All rights reserved. 4 // SPDX-License-Identifier: MIT 5 6 package repo 7 8 import ( 9 "errors" 10 "fmt" 11 "html" 12 "net/http" 13 "net/url" 14 "strconv" 15 "strings" 16 "time" 17 18 "code.gitea.io/gitea/models" 19 activities_model "code.gitea.io/gitea/models/activities" 20 "code.gitea.io/gitea/models/db" 21 git_model "code.gitea.io/gitea/models/git" 22 issues_model "code.gitea.io/gitea/models/issues" 23 "code.gitea.io/gitea/models/organization" 24 access_model "code.gitea.io/gitea/models/perm/access" 25 pull_model "code.gitea.io/gitea/models/pull" 26 repo_model "code.gitea.io/gitea/models/repo" 27 "code.gitea.io/gitea/models/unit" 28 user_model "code.gitea.io/gitea/models/user" 29 "code.gitea.io/gitea/modules/base" 30 "code.gitea.io/gitea/modules/context" 31 "code.gitea.io/gitea/modules/git" 32 issue_template "code.gitea.io/gitea/modules/issue/template" 33 "code.gitea.io/gitea/modules/log" 34 "code.gitea.io/gitea/modules/setting" 35 "code.gitea.io/gitea/modules/structs" 36 "code.gitea.io/gitea/modules/upload" 37 "code.gitea.io/gitea/modules/util" 38 "code.gitea.io/gitea/modules/web" 39 "code.gitea.io/gitea/routers/utils" 40 asymkey_service "code.gitea.io/gitea/services/asymkey" 41 "code.gitea.io/gitea/services/automerge" 42 "code.gitea.io/gitea/services/forms" 43 "code.gitea.io/gitea/services/gitdiff" 44 notify_service "code.gitea.io/gitea/services/notify" 45 pull_service "code.gitea.io/gitea/services/pull" 46 repo_service "code.gitea.io/gitea/services/repository" 47 48 "github.com/gobwas/glob" 49 ) 50 51 const ( 52 tplFork base.TplName = "repo/pulls/fork" 53 tplCompareDiff base.TplName = "repo/diff/compare" 54 tplPullCommits base.TplName = "repo/pulls/commits" 55 tplPullFiles base.TplName = "repo/pulls/files" 56 57 pullRequestTemplateKey = "PullRequestTemplate" 58 ) 59 60 var pullRequestTemplateCandidates = []string{ 61 "PULL_REQUEST_TEMPLATE.md", 62 "PULL_REQUEST_TEMPLATE.yaml", 63 "PULL_REQUEST_TEMPLATE.yml", 64 "pull_request_template.md", 65 "pull_request_template.yaml", 66 "pull_request_template.yml", 67 ".gitea/PULL_REQUEST_TEMPLATE.md", 68 ".gitea/PULL_REQUEST_TEMPLATE.yaml", 69 ".gitea/PULL_REQUEST_TEMPLATE.yml", 70 ".gitea/pull_request_template.md", 71 ".gitea/pull_request_template.yaml", 72 ".gitea/pull_request_template.yml", 73 ".github/PULL_REQUEST_TEMPLATE.md", 74 ".github/PULL_REQUEST_TEMPLATE.yaml", 75 ".github/PULL_REQUEST_TEMPLATE.yml", 76 ".github/pull_request_template.md", 77 ".github/pull_request_template.yaml", 78 ".github/pull_request_template.yml", 79 } 80 81 func getRepository(ctx *context.Context, repoID int64) *repo_model.Repository { 82 repo, err := repo_model.GetRepositoryByID(ctx, repoID) 83 if err != nil { 84 if repo_model.IsErrRepoNotExist(err) { 85 ctx.NotFound("GetRepositoryByID", nil) 86 } else { 87 ctx.ServerError("GetRepositoryByID", err) 88 } 89 return nil 90 } 91 92 perm, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer) 93 if err != nil { 94 ctx.ServerError("GetUserRepoPermission", err) 95 return nil 96 } 97 98 if !perm.CanRead(unit.TypeCode) { 99 log.Trace("Permission Denied: User %-v cannot read %-v of repo %-v\n"+ 100 "User in repo has Permissions: %-+v", 101 ctx.Doer, 102 unit.TypeCode, 103 ctx.Repo, 104 perm) 105 ctx.NotFound("getRepository", nil) 106 return nil 107 } 108 return repo 109 } 110 111 func getForkRepository(ctx *context.Context) *repo_model.Repository { 112 forkRepo := getRepository(ctx, ctx.ParamsInt64(":repoid")) 113 if ctx.Written() { 114 return nil 115 } 116 117 if forkRepo.IsEmpty { 118 log.Trace("Empty repository %-v", forkRepo) 119 ctx.NotFound("getForkRepository", nil) 120 return nil 121 } 122 123 if err := forkRepo.LoadOwner(ctx); err != nil { 124 ctx.ServerError("LoadOwner", err) 125 return nil 126 } 127 128 ctx.Data["repo_name"] = forkRepo.Name 129 ctx.Data["description"] = forkRepo.Description 130 ctx.Data["IsPrivate"] = forkRepo.IsPrivate || forkRepo.Owner.Visibility == structs.VisibleTypePrivate 131 canForkToUser := forkRepo.OwnerID != ctx.Doer.ID && !repo_model.HasForkedRepo(ctx, ctx.Doer.ID, forkRepo.ID) 132 133 ctx.Data["ForkRepo"] = forkRepo 134 135 ownedOrgs, err := organization.GetOrgsCanCreateRepoByUserID(ctx, ctx.Doer.ID) 136 if err != nil { 137 ctx.ServerError("GetOrgsCanCreateRepoByUserID", err) 138 return nil 139 } 140 var orgs []*organization.Organization 141 for _, org := range ownedOrgs { 142 if forkRepo.OwnerID != org.ID && !repo_model.HasForkedRepo(ctx, org.ID, forkRepo.ID) { 143 orgs = append(orgs, org) 144 } 145 } 146 147 traverseParentRepo := forkRepo 148 for { 149 if ctx.Doer.ID == traverseParentRepo.OwnerID { 150 canForkToUser = false 151 } else { 152 for i, org := range orgs { 153 if org.ID == traverseParentRepo.OwnerID { 154 orgs = append(orgs[:i], orgs[i+1:]...) 155 break 156 } 157 } 158 } 159 160 if !traverseParentRepo.IsFork { 161 break 162 } 163 traverseParentRepo, err = repo_model.GetRepositoryByID(ctx, traverseParentRepo.ForkID) 164 if err != nil { 165 ctx.ServerError("GetRepositoryByID", err) 166 return nil 167 } 168 } 169 170 ctx.Data["CanForkToUser"] = canForkToUser 171 ctx.Data["Orgs"] = orgs 172 173 if canForkToUser { 174 ctx.Data["ContextUser"] = ctx.Doer 175 } else if len(orgs) > 0 { 176 ctx.Data["ContextUser"] = orgs[0] 177 } else { 178 ctx.Data["CanForkRepo"] = false 179 ctx.Flash.Error(ctx.Tr("repo.fork_no_valid_owners"), true) 180 return nil 181 } 182 183 return forkRepo 184 } 185 186 // Fork render repository fork page 187 func Fork(ctx *context.Context) { 188 ctx.Data["Title"] = ctx.Tr("new_fork") 189 190 if ctx.Doer.CanForkRepo() { 191 ctx.Data["CanForkRepo"] = true 192 } else { 193 maxCreationLimit := ctx.Doer.MaxCreationLimit() 194 msg := ctx.TrN(maxCreationLimit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", maxCreationLimit) 195 ctx.Flash.Error(msg, true) 196 } 197 198 getForkRepository(ctx) 199 if ctx.Written() { 200 return 201 } 202 203 ctx.HTML(http.StatusOK, tplFork) 204 } 205 206 // ForkPost response for forking a repository 207 func ForkPost(ctx *context.Context) { 208 form := web.GetForm(ctx).(*forms.CreateRepoForm) 209 ctx.Data["Title"] = ctx.Tr("new_fork") 210 ctx.Data["CanForkRepo"] = true 211 212 ctxUser := checkContextUser(ctx, form.UID) 213 if ctx.Written() { 214 return 215 } 216 217 forkRepo := getForkRepository(ctx) 218 if ctx.Written() { 219 return 220 } 221 222 ctx.Data["ContextUser"] = ctxUser 223 224 if ctx.HasError() { 225 ctx.HTML(http.StatusOK, tplFork) 226 return 227 } 228 229 var err error 230 traverseParentRepo := forkRepo 231 for { 232 if ctxUser.ID == traverseParentRepo.OwnerID { 233 ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_has_same_repo"), tplFork, &form) 234 return 235 } 236 repo := repo_model.GetForkedRepo(ctx, ctxUser.ID, traverseParentRepo.ID) 237 if repo != nil { 238 ctx.Redirect(ctxUser.HomeLink() + "/" + url.PathEscape(repo.Name)) 239 return 240 } 241 if !traverseParentRepo.IsFork { 242 break 243 } 244 traverseParentRepo, err = repo_model.GetRepositoryByID(ctx, traverseParentRepo.ForkID) 245 if err != nil { 246 ctx.ServerError("GetRepositoryByID", err) 247 return 248 } 249 } 250 251 // Check if user is allowed to create repo's on the organization. 252 if ctxUser.IsOrganization() { 253 isAllowedToFork, err := organization.OrgFromUser(ctxUser).CanCreateOrgRepo(ctx.Doer.ID) 254 if err != nil { 255 ctx.ServerError("CanCreateOrgRepo", err) 256 return 257 } else if !isAllowedToFork { 258 ctx.Error(http.StatusForbidden) 259 return 260 } 261 } 262 263 repo, err := repo_service.ForkRepository(ctx, ctx.Doer, ctxUser, repo_service.ForkRepoOptions{ 264 BaseRepo: forkRepo, 265 Name: form.RepoName, 266 Description: form.Description, 267 }) 268 if err != nil { 269 ctx.Data["Err_RepoName"] = true 270 switch { 271 case repo_model.IsErrReachLimitOfRepo(err): 272 maxCreationLimit := ctxUser.MaxCreationLimit() 273 msg := ctx.TrN(maxCreationLimit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", maxCreationLimit) 274 ctx.RenderWithErr(msg, tplFork, &form) 275 case repo_model.IsErrRepoAlreadyExist(err): 276 ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_has_same_repo"), tplFork, &form) 277 case repo_model.IsErrRepoFilesAlreadyExist(err): 278 switch { 279 case ctx.IsUserSiteAdmin() || (setting.Repository.AllowAdoptionOfUnadoptedRepositories && setting.Repository.AllowDeleteOfUnadoptedRepositories): 280 ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt_or_delete"), tplFork, form) 281 case setting.Repository.AllowAdoptionOfUnadoptedRepositories: 282 ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt"), tplFork, form) 283 case setting.Repository.AllowDeleteOfUnadoptedRepositories: 284 ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.delete"), tplFork, form) 285 default: 286 ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist"), tplFork, form) 287 } 288 case db.IsErrNameReserved(err): 289 ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name), tplFork, &form) 290 case db.IsErrNamePatternNotAllowed(err): 291 ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tplFork, &form) 292 default: 293 ctx.ServerError("ForkPost", err) 294 } 295 return 296 } 297 298 log.Trace("Repository forked[%d]: %s/%s", forkRepo.ID, ctxUser.Name, repo.Name) 299 ctx.Redirect(ctxUser.HomeLink() + "/" + url.PathEscape(repo.Name)) 300 } 301 302 func getPullInfo(ctx *context.Context) (issue *issues_model.Issue, ok bool) { 303 issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) 304 if err != nil { 305 if issues_model.IsErrIssueNotExist(err) { 306 ctx.NotFound("GetIssueByIndex", err) 307 } else { 308 ctx.ServerError("GetIssueByIndex", err) 309 } 310 return nil, false 311 } 312 if err = issue.LoadPoster(ctx); err != nil { 313 ctx.ServerError("LoadPoster", err) 314 return nil, false 315 } 316 if err := issue.LoadRepo(ctx); err != nil { 317 ctx.ServerError("LoadRepo", err) 318 return nil, false 319 } 320 ctx.Data["Title"] = fmt.Sprintf("#%d - %s", issue.Index, issue.Title) 321 ctx.Data["Issue"] = issue 322 323 if !issue.IsPull { 324 ctx.NotFound("ViewPullCommits", nil) 325 return nil, false 326 } 327 328 if err = issue.LoadPullRequest(ctx); err != nil { 329 ctx.ServerError("LoadPullRequest", err) 330 return nil, false 331 } 332 333 if err = issue.PullRequest.LoadHeadRepo(ctx); err != nil { 334 ctx.ServerError("LoadHeadRepo", err) 335 return nil, false 336 } 337 338 if ctx.IsSigned { 339 // Update issue-user. 340 if err = activities_model.SetIssueReadBy(ctx, issue.ID, ctx.Doer.ID); err != nil { 341 ctx.ServerError("ReadBy", err) 342 return nil, false 343 } 344 } 345 346 return issue, true 347 } 348 349 func setMergeTarget(ctx *context.Context, pull *issues_model.PullRequest) { 350 if ctx.Repo.Owner.Name == pull.MustHeadUserName(ctx) { 351 ctx.Data["HeadTarget"] = pull.HeadBranch 352 } else if pull.HeadRepo == nil { 353 ctx.Data["HeadTarget"] = pull.MustHeadUserName(ctx) + ":" + pull.HeadBranch 354 } else { 355 ctx.Data["HeadTarget"] = pull.MustHeadUserName(ctx) + "/" + pull.HeadRepo.Name + ":" + pull.HeadBranch 356 } 357 ctx.Data["BaseTarget"] = pull.BaseBranch 358 ctx.Data["HeadBranchLink"] = pull.GetHeadBranchLink() 359 ctx.Data["BaseBranchLink"] = pull.GetBaseBranchLink() 360 } 361 362 // GetPullDiffStats get Pull Requests diff stats 363 func GetPullDiffStats(ctx *context.Context) { 364 issue, ok := getPullInfo(ctx) 365 if !ok { 366 return 367 } 368 pull := issue.PullRequest 369 370 mergeBaseCommitID := GetMergedBaseCommitID(ctx, issue) 371 372 if mergeBaseCommitID == "" { 373 ctx.NotFound("PullFiles", nil) 374 return 375 } 376 377 headCommitID, err := ctx.Repo.GitRepo.GetRefCommitID(pull.GetGitRefName()) 378 if err != nil { 379 ctx.ServerError("GetRefCommitID", err) 380 return 381 } 382 383 diffOptions := &gitdiff.DiffOptions{ 384 BeforeCommitID: mergeBaseCommitID, 385 AfterCommitID: headCommitID, 386 MaxLines: setting.Git.MaxGitDiffLines, 387 MaxLineCharacters: setting.Git.MaxGitDiffLineCharacters, 388 MaxFiles: setting.Git.MaxGitDiffFiles, 389 WhitespaceBehavior: gitdiff.GetWhitespaceFlag(ctx.Data["WhitespaceBehavior"].(string)), 390 } 391 392 diff, err := gitdiff.GetPullDiffStats(ctx.Repo.GitRepo, diffOptions) 393 if err != nil { 394 ctx.ServerError("GetPullDiffStats", err) 395 return 396 } 397 398 ctx.Data["Diff"] = diff 399 } 400 401 func GetMergedBaseCommitID(ctx *context.Context, issue *issues_model.Issue) string { 402 pull := issue.PullRequest 403 404 var baseCommit string 405 // Some migrated PR won't have any Base SHA and lose history, try to get one 406 if pull.MergeBase == "" { 407 var commitSHA, parentCommit string 408 // If there is a head or a patch file, and it is readable, grab info 409 commitSHA, err := ctx.Repo.GitRepo.GetRefCommitID(pull.GetGitRefName()) 410 if err != nil { 411 // Head File does not exist, try the patch 412 commitSHA, err = ctx.Repo.GitRepo.ReadPatchCommit(pull.Index) 413 if err == nil { 414 // Recreate pull head in files for next time 415 if err := ctx.Repo.GitRepo.SetReference(pull.GetGitRefName(), commitSHA); err != nil { 416 log.Error("Could not write head file", err) 417 } 418 } else { 419 // There is no history available 420 log.Trace("No history file available for PR %d", pull.Index) 421 } 422 } 423 if commitSHA != "" { 424 // Get immediate parent of the first commit in the patch, grab history back 425 parentCommit, _, err = git.NewCommand(ctx, "rev-list", "-1", "--skip=1").AddDynamicArguments(commitSHA).RunStdString(&git.RunOpts{Dir: ctx.Repo.GitRepo.Path}) 426 if err == nil { 427 parentCommit = strings.TrimSpace(parentCommit) 428 } 429 // Special case on Git < 2.25 that doesn't fail on immediate empty history 430 if err != nil || parentCommit == "" { 431 log.Info("No known parent commit for PR %d, error: %v", pull.Index, err) 432 // bring at least partial history if it can work 433 parentCommit = commitSHA 434 } 435 } 436 baseCommit = parentCommit 437 } else { 438 // Keep an empty history or original commit 439 baseCommit = pull.MergeBase 440 } 441 442 return baseCommit 443 } 444 445 // PrepareMergedViewPullInfo show meta information for a merged pull request view page 446 func PrepareMergedViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git.CompareInfo { 447 pull := issue.PullRequest 448 449 setMergeTarget(ctx, pull) 450 ctx.Data["HasMerged"] = true 451 452 baseCommit := GetMergedBaseCommitID(ctx, issue) 453 454 compareInfo, err := ctx.Repo.GitRepo.GetCompareInfo(ctx.Repo.Repository.RepoPath(), 455 baseCommit, pull.GetGitRefName(), false, false) 456 if err != nil { 457 if strings.Contains(err.Error(), "fatal: Not a valid object name") || strings.Contains(err.Error(), "unknown revision or path not in the working tree") { 458 ctx.Data["IsPullRequestBroken"] = true 459 ctx.Data["BaseTarget"] = pull.BaseBranch 460 ctx.Data["NumCommits"] = 0 461 ctx.Data["NumFiles"] = 0 462 return nil 463 } 464 465 ctx.ServerError("GetCompareInfo", err) 466 return nil 467 } 468 ctx.Data["NumCommits"] = len(compareInfo.Commits) 469 ctx.Data["NumFiles"] = compareInfo.NumFiles 470 471 if len(compareInfo.Commits) != 0 { 472 sha := compareInfo.Commits[0].ID.String() 473 commitStatuses, _, err := git_model.GetLatestCommitStatus(ctx, ctx.Repo.Repository.ID, sha, db.ListOptions{ListAll: true}) 474 if err != nil { 475 ctx.ServerError("GetLatestCommitStatus", err) 476 return nil 477 } 478 if len(commitStatuses) != 0 { 479 ctx.Data["LatestCommitStatuses"] = commitStatuses 480 ctx.Data["LatestCommitStatus"] = git_model.CalcCommitStatus(commitStatuses) 481 } 482 } 483 484 return compareInfo 485 } 486 487 // PrepareViewPullInfo show meta information for a pull request preview page 488 func PrepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git.CompareInfo { 489 ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes 490 491 repo := ctx.Repo.Repository 492 pull := issue.PullRequest 493 494 if err := pull.LoadHeadRepo(ctx); err != nil { 495 ctx.ServerError("LoadHeadRepo", err) 496 return nil 497 } 498 499 if err := pull.LoadBaseRepo(ctx); err != nil { 500 ctx.ServerError("LoadBaseRepo", err) 501 return nil 502 } 503 504 setMergeTarget(ctx, pull) 505 506 pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, repo.ID, pull.BaseBranch) 507 if err != nil { 508 ctx.ServerError("LoadProtectedBranch", err) 509 return nil 510 } 511 ctx.Data["EnableStatusCheck"] = pb != nil && pb.EnableStatusCheck 512 513 var baseGitRepo *git.Repository 514 if pull.BaseRepoID == ctx.Repo.Repository.ID && ctx.Repo.GitRepo != nil { 515 baseGitRepo = ctx.Repo.GitRepo 516 } else { 517 baseGitRepo, err := git.OpenRepository(ctx, pull.BaseRepo.RepoPath()) 518 if err != nil { 519 ctx.ServerError("OpenRepository", err) 520 return nil 521 } 522 defer baseGitRepo.Close() 523 } 524 525 if !baseGitRepo.IsBranchExist(pull.BaseBranch) { 526 ctx.Data["IsPullRequestBroken"] = true 527 ctx.Data["BaseTarget"] = pull.BaseBranch 528 ctx.Data["HeadTarget"] = pull.HeadBranch 529 530 sha, err := baseGitRepo.GetRefCommitID(pull.GetGitRefName()) 531 if err != nil { 532 ctx.ServerError(fmt.Sprintf("GetRefCommitID(%s)", pull.GetGitRefName()), err) 533 return nil 534 } 535 commitStatuses, _, err := git_model.GetLatestCommitStatus(ctx, repo.ID, sha, db.ListOptions{ListAll: true}) 536 if err != nil { 537 ctx.ServerError("GetLatestCommitStatus", err) 538 return nil 539 } 540 if len(commitStatuses) > 0 { 541 ctx.Data["LatestCommitStatuses"] = commitStatuses 542 ctx.Data["LatestCommitStatus"] = git_model.CalcCommitStatus(commitStatuses) 543 } 544 545 compareInfo, err := baseGitRepo.GetCompareInfo(pull.BaseRepo.RepoPath(), 546 pull.MergeBase, pull.GetGitRefName(), false, false) 547 if err != nil { 548 if strings.Contains(err.Error(), "fatal: Not a valid object name") { 549 ctx.Data["IsPullRequestBroken"] = true 550 ctx.Data["BaseTarget"] = pull.BaseBranch 551 ctx.Data["NumCommits"] = 0 552 ctx.Data["NumFiles"] = 0 553 return nil 554 } 555 556 ctx.ServerError("GetCompareInfo", err) 557 return nil 558 } 559 560 ctx.Data["NumCommits"] = len(compareInfo.Commits) 561 ctx.Data["NumFiles"] = compareInfo.NumFiles 562 return compareInfo 563 } 564 565 var headBranchExist bool 566 var headBranchSha string 567 // HeadRepo may be missing 568 if pull.HeadRepo != nil { 569 headGitRepo, err := git.OpenRepository(ctx, pull.HeadRepo.RepoPath()) 570 if err != nil { 571 ctx.ServerError("OpenRepository", err) 572 return nil 573 } 574 defer headGitRepo.Close() 575 576 if pull.Flow == issues_model.PullRequestFlowGithub { 577 headBranchExist = headGitRepo.IsBranchExist(pull.HeadBranch) 578 } else { 579 headBranchExist = git.IsReferenceExist(ctx, baseGitRepo.Path, pull.GetGitRefName()) 580 } 581 582 if headBranchExist { 583 if pull.Flow != issues_model.PullRequestFlowGithub { 584 headBranchSha, err = baseGitRepo.GetRefCommitID(pull.GetGitRefName()) 585 } else { 586 headBranchSha, err = headGitRepo.GetBranchCommitID(pull.HeadBranch) 587 } 588 if err != nil { 589 ctx.ServerError("GetBranchCommitID", err) 590 return nil 591 } 592 } 593 } 594 595 if headBranchExist { 596 var err error 597 ctx.Data["UpdateAllowed"], ctx.Data["UpdateByRebaseAllowed"], err = pull_service.IsUserAllowedToUpdate(ctx, pull, ctx.Doer) 598 if err != nil { 599 ctx.ServerError("IsUserAllowedToUpdate", err) 600 return nil 601 } 602 ctx.Data["GetCommitMessages"] = pull_service.GetSquashMergeCommitMessages(ctx, pull) 603 } else { 604 ctx.Data["GetCommitMessages"] = "" 605 } 606 607 sha, err := baseGitRepo.GetRefCommitID(pull.GetGitRefName()) 608 if err != nil { 609 if git.IsErrNotExist(err) { 610 ctx.Data["IsPullRequestBroken"] = true 611 if pull.IsSameRepo() { 612 ctx.Data["HeadTarget"] = pull.HeadBranch 613 } else if pull.HeadRepo == nil { 614 ctx.Data["HeadTarget"] = ctx.Locale.Tr("repo.pull.deleted_branch", pull.HeadBranch) 615 } else { 616 ctx.Data["HeadTarget"] = pull.HeadRepo.OwnerName + ":" + pull.HeadBranch 617 } 618 ctx.Data["BaseTarget"] = pull.BaseBranch 619 ctx.Data["NumCommits"] = 0 620 ctx.Data["NumFiles"] = 0 621 return nil 622 } 623 ctx.ServerError(fmt.Sprintf("GetRefCommitID(%s)", pull.GetGitRefName()), err) 624 return nil 625 } 626 627 commitStatuses, _, err := git_model.GetLatestCommitStatus(ctx, repo.ID, sha, db.ListOptions{ListAll: true}) 628 if err != nil { 629 ctx.ServerError("GetLatestCommitStatus", err) 630 return nil 631 } 632 if len(commitStatuses) > 0 { 633 ctx.Data["LatestCommitStatuses"] = commitStatuses 634 ctx.Data["LatestCommitStatus"] = git_model.CalcCommitStatus(commitStatuses) 635 } 636 637 if pb != nil && pb.EnableStatusCheck { 638 639 var missingRequiredChecks []string 640 for _, requiredContext := range pb.StatusCheckContexts { 641 contextFound := false 642 matchesRequiredContext := createRequiredContextMatcher(requiredContext) 643 for _, presentStatus := range commitStatuses { 644 if matchesRequiredContext(presentStatus.Context) { 645 contextFound = true 646 break 647 } 648 } 649 650 if !contextFound { 651 missingRequiredChecks = append(missingRequiredChecks, requiredContext) 652 } 653 } 654 ctx.Data["MissingRequiredChecks"] = missingRequiredChecks 655 656 ctx.Data["is_context_required"] = func(context string) bool { 657 for _, c := range pb.StatusCheckContexts { 658 if c == context { 659 return true 660 } 661 if gp, err := glob.Compile(c); err != nil { 662 // All newly created status_check_contexts are checked to ensure they are valid glob expressions before being stored in the database. 663 // But some old status_check_context created before glob was introduced may be invalid glob expressions. 664 // So log the error here for debugging. 665 log.Error("compile glob %q: %v", c, err) 666 } else if gp.Match(context) { 667 return true 668 } 669 } 670 return false 671 } 672 ctx.Data["RequiredStatusCheckState"] = pull_service.MergeRequiredContextsCommitStatus(commitStatuses, pb.StatusCheckContexts) 673 } 674 675 ctx.Data["HeadBranchMovedOn"] = headBranchSha != sha 676 ctx.Data["HeadBranchCommitID"] = headBranchSha 677 ctx.Data["PullHeadCommitID"] = sha 678 679 if pull.HeadRepo == nil || !headBranchExist || (!pull.Issue.IsClosed && (headBranchSha != sha)) { 680 ctx.Data["IsPullRequestBroken"] = true 681 if pull.IsSameRepo() { 682 ctx.Data["HeadTarget"] = pull.HeadBranch 683 } else if pull.HeadRepo == nil { 684 ctx.Data["HeadTarget"] = ctx.Locale.Tr("repo.pull.deleted_branch", pull.HeadBranch) 685 } else { 686 ctx.Data["HeadTarget"] = pull.HeadRepo.OwnerName + ":" + pull.HeadBranch 687 } 688 } 689 690 compareInfo, err := baseGitRepo.GetCompareInfo(pull.BaseRepo.RepoPath(), 691 git.BranchPrefix+pull.BaseBranch, pull.GetGitRefName(), false, false) 692 if err != nil { 693 if strings.Contains(err.Error(), "fatal: Not a valid object name") { 694 ctx.Data["IsPullRequestBroken"] = true 695 ctx.Data["BaseTarget"] = pull.BaseBranch 696 ctx.Data["NumCommits"] = 0 697 ctx.Data["NumFiles"] = 0 698 return nil 699 } 700 701 ctx.ServerError("GetCompareInfo", err) 702 return nil 703 } 704 705 if compareInfo.HeadCommitID == compareInfo.MergeBase { 706 ctx.Data["IsNothingToCompare"] = true 707 } 708 709 if pull.IsWorkInProgress() { 710 ctx.Data["IsPullWorkInProgress"] = true 711 ctx.Data["WorkInProgressPrefix"] = pull.GetWorkInProgressPrefix(ctx) 712 } 713 714 if pull.IsFilesConflicted() { 715 ctx.Data["IsPullFilesConflicted"] = true 716 ctx.Data["ConflictedFiles"] = pull.ConflictedFiles 717 } 718 719 ctx.Data["NumCommits"] = len(compareInfo.Commits) 720 ctx.Data["NumFiles"] = compareInfo.NumFiles 721 return compareInfo 722 } 723 724 func createRequiredContextMatcher(requiredContext string) func(string) bool { 725 if gp, err := glob.Compile(requiredContext); err == nil { 726 return func(contextToCheck string) bool { 727 return gp.Match(contextToCheck) 728 } 729 } 730 731 return func(contextToCheck string) bool { 732 return requiredContext == contextToCheck 733 } 734 } 735 736 type pullCommitList struct { 737 Commits []pull_service.CommitInfo `json:"commits"` 738 LastReviewCommitSha string `json:"last_review_commit_sha"` 739 Locale map[string]string `json:"locale"` 740 } 741 742 // GetPullCommits get all commits for given pull request 743 func GetPullCommits(ctx *context.Context) { 744 issue, ok := getPullInfo(ctx) 745 if !ok { 746 return 747 } 748 resp := &pullCommitList{} 749 750 commits, lastReviewCommitSha, err := pull_service.GetPullCommits(ctx, issue) 751 if err != nil { 752 ctx.JSON(http.StatusInternalServerError, err) 753 return 754 } 755 756 // Get the needed locale 757 resp.Locale = map[string]string{ 758 "lang": ctx.Locale.Language(), 759 "show_all_commits": ctx.Tr("repo.pulls.show_all_commits"), 760 "stats_num_commits": ctx.TrN(len(commits), "repo.activity.git_stats_commit_1", "repo.activity.git_stats_commit_n", len(commits)), 761 "show_changes_since_your_last_review": ctx.Tr("repo.pulls.show_changes_since_your_last_review"), 762 "select_commit_hold_shift_for_range": ctx.Tr("repo.pulls.select_commit_hold_shift_for_range"), 763 } 764 765 resp.Commits = commits 766 resp.LastReviewCommitSha = lastReviewCommitSha 767 768 ctx.JSON(http.StatusOK, resp) 769 } 770 771 // ViewPullCommits show commits for a pull request 772 func ViewPullCommits(ctx *context.Context) { 773 ctx.Data["PageIsPullList"] = true 774 ctx.Data["PageIsPullCommits"] = true 775 776 issue, ok := getPullInfo(ctx) 777 if !ok { 778 return 779 } 780 pull := issue.PullRequest 781 782 var prInfo *git.CompareInfo 783 if pull.HasMerged { 784 prInfo = PrepareMergedViewPullInfo(ctx, issue) 785 } else { 786 prInfo = PrepareViewPullInfo(ctx, issue) 787 } 788 789 if ctx.Written() { 790 return 791 } else if prInfo == nil { 792 ctx.NotFound("ViewPullCommits", nil) 793 return 794 } 795 796 ctx.Data["Username"] = ctx.Repo.Owner.Name 797 ctx.Data["Reponame"] = ctx.Repo.Repository.Name 798 799 commits := git_model.ConvertFromGitCommit(ctx, prInfo.Commits, ctx.Repo.Repository) 800 ctx.Data["Commits"] = commits 801 ctx.Data["CommitCount"] = len(commits) 802 803 ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) 804 ctx.Data["IsIssuePoster"] = ctx.IsSigned && issue.IsPoster(ctx.Doer.ID) 805 806 // For PR commits page 807 PrepareBranchList(ctx) 808 if ctx.Written() { 809 return 810 } 811 getBranchData(ctx, issue) 812 ctx.HTML(http.StatusOK, tplPullCommits) 813 } 814 815 // ViewPullFiles render pull request changed files list page 816 func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommit string, willShowSpecifiedCommitRange, willShowSpecifiedCommit bool) { 817 ctx.Data["PageIsPullList"] = true 818 ctx.Data["PageIsPullFiles"] = true 819 820 issue, ok := getPullInfo(ctx) 821 if !ok { 822 return 823 } 824 pull := issue.PullRequest 825 826 var ( 827 startCommitID string 828 endCommitID string 829 gitRepo = ctx.Repo.GitRepo 830 ) 831 832 var prInfo *git.CompareInfo 833 if pull.HasMerged { 834 prInfo = PrepareMergedViewPullInfo(ctx, issue) 835 } else { 836 prInfo = PrepareViewPullInfo(ctx, issue) 837 } 838 839 // Validate the given commit sha to show (if any passed) 840 if willShowSpecifiedCommit || willShowSpecifiedCommitRange { 841 842 foundStartCommit := len(specifiedStartCommit) == 0 843 foundEndCommit := len(specifiedEndCommit) == 0 844 845 if !(foundStartCommit && foundEndCommit) { 846 for _, commit := range prInfo.Commits { 847 if commit.ID.String() == specifiedStartCommit { 848 foundStartCommit = true 849 } 850 if commit.ID.String() == specifiedEndCommit { 851 foundEndCommit = true 852 } 853 854 if foundStartCommit && foundEndCommit { 855 break 856 } 857 } 858 } 859 860 if !(foundStartCommit && foundEndCommit) { 861 ctx.NotFound("Given SHA1 not found for this PR", nil) 862 return 863 } 864 } 865 866 if ctx.Written() { 867 return 868 } else if prInfo == nil { 869 ctx.NotFound("ViewPullFiles", nil) 870 return 871 } 872 873 headCommitID, err := gitRepo.GetRefCommitID(pull.GetGitRefName()) 874 if err != nil { 875 ctx.ServerError("GetRefCommitID", err) 876 return 877 } 878 879 ctx.Data["IsShowingOnlySingleCommit"] = willShowSpecifiedCommit 880 881 if willShowSpecifiedCommit || willShowSpecifiedCommitRange { 882 if len(specifiedEndCommit) > 0 { 883 endCommitID = specifiedEndCommit 884 } else { 885 endCommitID = headCommitID 886 } 887 if len(specifiedStartCommit) > 0 { 888 startCommitID = specifiedStartCommit 889 } else { 890 startCommitID = prInfo.MergeBase 891 } 892 ctx.Data["IsShowingAllCommits"] = false 893 } else { 894 endCommitID = headCommitID 895 startCommitID = prInfo.MergeBase 896 ctx.Data["IsShowingAllCommits"] = true 897 } 898 899 ctx.Data["Username"] = ctx.Repo.Owner.Name 900 ctx.Data["Reponame"] = ctx.Repo.Repository.Name 901 ctx.Data["AfterCommitID"] = endCommitID 902 ctx.Data["BeforeCommitID"] = startCommitID 903 904 fileOnly := ctx.FormBool("file-only") 905 906 maxLines, maxFiles := setting.Git.MaxGitDiffLines, setting.Git.MaxGitDiffFiles 907 files := ctx.FormStrings("files") 908 if fileOnly && (len(files) == 2 || len(files) == 1) { 909 maxLines, maxFiles = -1, -1 910 } 911 912 diffOptions := &gitdiff.DiffOptions{ 913 AfterCommitID: endCommitID, 914 SkipTo: ctx.FormString("skip-to"), 915 MaxLines: maxLines, 916 MaxLineCharacters: setting.Git.MaxGitDiffLineCharacters, 917 MaxFiles: maxFiles, 918 WhitespaceBehavior: gitdiff.GetWhitespaceFlag(ctx.Data["WhitespaceBehavior"].(string)), 919 } 920 921 if !willShowSpecifiedCommit { 922 diffOptions.BeforeCommitID = startCommitID 923 } 924 925 var methodWithError string 926 var diff *gitdiff.Diff 927 928 // if we're not logged in or only a single commit (or commit range) is shown we 929 // have to load only the diff and not get the viewed information 930 // as the viewed information is designed to be loaded only on latest PR 931 // diff and if you're signed in. 932 if !ctx.IsSigned || willShowSpecifiedCommit || willShowSpecifiedCommitRange { 933 diff, err = gitdiff.GetDiff(gitRepo, diffOptions, files...) 934 methodWithError = "GetDiff" 935 } else { 936 diff, err = gitdiff.SyncAndGetUserSpecificDiff(ctx, ctx.Doer.ID, pull, gitRepo, diffOptions, files...) 937 methodWithError = "SyncAndGetUserSpecificDiff" 938 } 939 if err != nil { 940 ctx.ServerError(methodWithError, err) 941 return 942 } 943 944 ctx.PageData["prReview"] = map[string]any{ 945 "numberOfFiles": diff.NumFiles, 946 "numberOfViewedFiles": diff.NumViewedFiles, 947 } 948 949 if err = diff.LoadComments(ctx, issue, ctx.Doer, ctx.Data["ShowOutdatedComments"].(bool)); err != nil { 950 ctx.ServerError("LoadComments", err) 951 return 952 } 953 954 pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pull.BaseRepoID, pull.BaseBranch) 955 if err != nil { 956 ctx.ServerError("LoadProtectedBranch", err) 957 return 958 } 959 960 if pb != nil { 961 glob := pb.GetProtectedFilePatterns() 962 if len(glob) != 0 { 963 for _, file := range diff.Files { 964 file.IsProtected = pb.IsProtectedFile(glob, file.Name) 965 } 966 } 967 } 968 969 ctx.Data["Diff"] = diff 970 ctx.Data["DiffNotAvailable"] = diff.NumFiles == 0 971 972 baseCommit, err := ctx.Repo.GitRepo.GetCommit(startCommitID) 973 if err != nil { 974 ctx.ServerError("GetCommit", err) 975 return 976 } 977 commit, err := gitRepo.GetCommit(endCommitID) 978 if err != nil { 979 ctx.ServerError("GetCommit", err) 980 return 981 } 982 983 if ctx.IsSigned && ctx.Doer != nil { 984 if ctx.Data["CanMarkConversation"], err = issues_model.CanMarkConversation(ctx, issue, ctx.Doer); err != nil { 985 ctx.ServerError("CanMarkConversation", err) 986 return 987 } 988 } 989 990 setCompareContext(ctx, baseCommit, commit, ctx.Repo.Owner.Name, ctx.Repo.Repository.Name) 991 992 assigneeUsers, err := repo_model.GetRepoAssignees(ctx, ctx.Repo.Repository) 993 if err != nil { 994 ctx.ServerError("GetRepoAssignees", err) 995 return 996 } 997 ctx.Data["Assignees"] = MakeSelfOnTop(ctx.Doer, assigneeUsers) 998 999 handleTeamMentions(ctx) 1000 if ctx.Written() { 1001 return 1002 } 1003 1004 currentReview, err := issues_model.GetCurrentReview(ctx, ctx.Doer, issue) 1005 if err != nil && !issues_model.IsErrReviewNotExist(err) { 1006 ctx.ServerError("GetCurrentReview", err) 1007 return 1008 } 1009 numPendingCodeComments := int64(0) 1010 if currentReview != nil { 1011 numPendingCodeComments, err = issues_model.CountComments(ctx, &issues_model.FindCommentsOptions{ 1012 Type: issues_model.CommentTypeCode, 1013 ReviewID: currentReview.ID, 1014 IssueID: issue.ID, 1015 }) 1016 if err != nil { 1017 ctx.ServerError("CountComments", err) 1018 return 1019 } 1020 } 1021 ctx.Data["CurrentReview"] = currentReview 1022 ctx.Data["PendingCodeCommentNumber"] = numPendingCodeComments 1023 1024 getBranchData(ctx, issue) 1025 ctx.Data["IsIssuePoster"] = ctx.IsSigned && issue.IsPoster(ctx.Doer.ID) 1026 ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) 1027 1028 ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled 1029 // For files changed page 1030 PrepareBranchList(ctx) 1031 if ctx.Written() { 1032 return 1033 } 1034 upload.AddUploadContext(ctx, "comment") 1035 1036 ctx.HTML(http.StatusOK, tplPullFiles) 1037 } 1038 1039 func ViewPullFilesForSingleCommit(ctx *context.Context) { 1040 viewPullFiles(ctx, "", ctx.Params("sha"), true, true) 1041 } 1042 1043 func ViewPullFilesForRange(ctx *context.Context) { 1044 viewPullFiles(ctx, ctx.Params("shaFrom"), ctx.Params("shaTo"), true, false) 1045 } 1046 1047 func ViewPullFilesStartingFromCommit(ctx *context.Context) { 1048 viewPullFiles(ctx, "", ctx.Params("sha"), true, false) 1049 } 1050 1051 func ViewPullFilesForAllCommitsOfPr(ctx *context.Context) { 1052 viewPullFiles(ctx, "", "", false, false) 1053 } 1054 1055 // UpdatePullRequest merge PR's baseBranch into headBranch 1056 func UpdatePullRequest(ctx *context.Context) { 1057 issue, ok := getPullInfo(ctx) 1058 if !ok { 1059 return 1060 } 1061 if issue.IsClosed { 1062 ctx.NotFound("MergePullRequest", nil) 1063 return 1064 } 1065 if issue.PullRequest.HasMerged { 1066 ctx.NotFound("MergePullRequest", nil) 1067 return 1068 } 1069 1070 rebase := ctx.FormString("style") == "rebase" 1071 1072 if err := issue.PullRequest.LoadBaseRepo(ctx); err != nil { 1073 ctx.ServerError("LoadBaseRepo", err) 1074 return 1075 } 1076 if err := issue.PullRequest.LoadHeadRepo(ctx); err != nil { 1077 ctx.ServerError("LoadHeadRepo", err) 1078 return 1079 } 1080 1081 allowedUpdateByMerge, allowedUpdateByRebase, err := pull_service.IsUserAllowedToUpdate(ctx, issue.PullRequest, ctx.Doer) 1082 if err != nil { 1083 ctx.ServerError("IsUserAllowedToMerge", err) 1084 return 1085 } 1086 1087 // ToDo: add check if maintainers are allowed to change branch ... (need migration & co) 1088 if (!allowedUpdateByMerge && !rebase) || (rebase && !allowedUpdateByRebase) { 1089 ctx.Flash.Error(ctx.Tr("repo.pulls.update_not_allowed")) 1090 ctx.Redirect(issue.Link()) 1091 return 1092 } 1093 1094 // default merge commit message 1095 message := fmt.Sprintf("Merge branch '%s' into %s", issue.PullRequest.BaseBranch, issue.PullRequest.HeadBranch) 1096 1097 if err = pull_service.Update(ctx, issue.PullRequest, ctx.Doer, message, rebase); err != nil { 1098 if models.IsErrMergeConflicts(err) { 1099 conflictError := err.(models.ErrMergeConflicts) 1100 flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{ 1101 "Message": ctx.Tr("repo.pulls.merge_conflict"), 1102 "Summary": ctx.Tr("repo.pulls.merge_conflict_summary"), 1103 "Details": utils.SanitizeFlashErrorString(conflictError.StdErr) + "<br>" + utils.SanitizeFlashErrorString(conflictError.StdOut), 1104 }) 1105 if err != nil { 1106 ctx.ServerError("UpdatePullRequest.HTMLString", err) 1107 return 1108 } 1109 ctx.Flash.Error(flashError) 1110 ctx.Redirect(issue.Link()) 1111 return 1112 } else if models.IsErrRebaseConflicts(err) { 1113 conflictError := err.(models.ErrRebaseConflicts) 1114 flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{ 1115 "Message": ctx.Tr("repo.pulls.rebase_conflict", utils.SanitizeFlashErrorString(conflictError.CommitSHA)), 1116 "Summary": ctx.Tr("repo.pulls.rebase_conflict_summary"), 1117 "Details": utils.SanitizeFlashErrorString(conflictError.StdErr) + "<br>" + utils.SanitizeFlashErrorString(conflictError.StdOut), 1118 }) 1119 if err != nil { 1120 ctx.ServerError("UpdatePullRequest.HTMLString", err) 1121 return 1122 } 1123 ctx.Flash.Error(flashError) 1124 ctx.Redirect(issue.Link()) 1125 return 1126 1127 } 1128 ctx.Flash.Error(err.Error()) 1129 ctx.Redirect(issue.Link()) 1130 return 1131 } 1132 1133 time.Sleep(1 * time.Second) 1134 1135 ctx.Flash.Success(ctx.Tr("repo.pulls.update_branch_success")) 1136 ctx.Redirect(issue.Link()) 1137 } 1138 1139 // MergePullRequest response for merging pull request 1140 func MergePullRequest(ctx *context.Context) { 1141 form := web.GetForm(ctx).(*forms.MergePullRequestForm) 1142 issue, ok := getPullInfo(ctx) 1143 if !ok { 1144 return 1145 } 1146 1147 pr := issue.PullRequest 1148 pr.Issue = issue 1149 pr.Issue.Repo = ctx.Repo.Repository 1150 1151 manuallyMerged := repo_model.MergeStyle(form.Do) == repo_model.MergeStyleManuallyMerged 1152 1153 mergeCheckType := pull_service.MergeCheckTypeGeneral 1154 if form.MergeWhenChecksSucceed { 1155 mergeCheckType = pull_service.MergeCheckTypeAuto 1156 } 1157 if manuallyMerged { 1158 mergeCheckType = pull_service.MergeCheckTypeManually 1159 } 1160 1161 // start with merging by checking 1162 if err := pull_service.CheckPullMergable(ctx, ctx.Doer, &ctx.Repo.Permission, pr, mergeCheckType, form.ForceMerge); err != nil { 1163 switch { 1164 case errors.Is(err, pull_service.ErrIsClosed): 1165 if issue.IsPull { 1166 ctx.JSONError(ctx.Tr("repo.pulls.is_closed")) 1167 } else { 1168 ctx.JSONError(ctx.Tr("repo.issues.closed_title")) 1169 } 1170 case errors.Is(err, pull_service.ErrUserNotAllowedToMerge): 1171 ctx.JSONError(ctx.Tr("repo.pulls.update_not_allowed")) 1172 case errors.Is(err, pull_service.ErrHasMerged): 1173 ctx.JSONError(ctx.Tr("repo.pulls.has_merged")) 1174 case errors.Is(err, pull_service.ErrIsWorkInProgress): 1175 ctx.JSONError(ctx.Tr("repo.pulls.no_merge_wip")) 1176 case errors.Is(err, pull_service.ErrNotMergableState): 1177 ctx.JSONError(ctx.Tr("repo.pulls.no_merge_not_ready")) 1178 case models.IsErrDisallowedToMerge(err): 1179 ctx.JSONError(ctx.Tr("repo.pulls.no_merge_not_ready")) 1180 case asymkey_service.IsErrWontSign(err): 1181 ctx.JSONError(err.Error()) // has no translation ... 1182 case errors.Is(err, pull_service.ErrDependenciesLeft): 1183 ctx.JSONError(ctx.Tr("repo.issues.dependency.pr_close_blocked")) 1184 default: 1185 ctx.ServerError("WebCheck", err) 1186 } 1187 1188 return 1189 } 1190 1191 // handle manually-merged mark 1192 if manuallyMerged { 1193 if err := pull_service.MergedManually(pr, ctx.Doer, ctx.Repo.GitRepo, form.MergeCommitID); err != nil { 1194 switch { 1195 case models.IsErrInvalidMergeStyle(err): 1196 ctx.JSONError(ctx.Tr("repo.pulls.invalid_merge_option")) 1197 case strings.Contains(err.Error(), "Wrong commit ID"): 1198 ctx.JSONError(ctx.Tr("repo.pulls.wrong_commit_id")) 1199 default: 1200 ctx.ServerError("MergedManually", err) 1201 } 1202 1203 return 1204 } 1205 1206 ctx.JSONRedirect(issue.Link()) 1207 return 1208 } 1209 1210 message := strings.TrimSpace(form.MergeTitleField) 1211 if len(message) == 0 { 1212 var err error 1213 message, _, err = pull_service.GetDefaultMergeMessage(ctx, ctx.Repo.GitRepo, pr, repo_model.MergeStyle(form.Do)) 1214 if err != nil { 1215 ctx.ServerError("GetDefaultMergeMessage", err) 1216 return 1217 } 1218 } 1219 1220 form.MergeMessageField = strings.TrimSpace(form.MergeMessageField) 1221 if len(form.MergeMessageField) > 0 { 1222 message += "\n\n" + form.MergeMessageField 1223 } 1224 1225 if form.MergeWhenChecksSucceed { 1226 // delete all scheduled auto merges 1227 _ = pull_model.DeleteScheduledAutoMerge(ctx, pr.ID) 1228 // schedule auto merge 1229 scheduled, err := automerge.ScheduleAutoMerge(ctx, ctx.Doer, pr, repo_model.MergeStyle(form.Do), message) 1230 if err != nil { 1231 ctx.ServerError("ScheduleAutoMerge", err) 1232 return 1233 } else if scheduled { 1234 // nothing more to do ... 1235 ctx.Flash.Success(ctx.Tr("repo.pulls.auto_merge_newly_scheduled")) 1236 ctx.JSONRedirect(fmt.Sprintf("%s/pulls/%d", ctx.Repo.RepoLink, pr.Index)) 1237 return 1238 } 1239 } 1240 1241 if err := pull_service.Merge(ctx, pr, ctx.Doer, ctx.Repo.GitRepo, repo_model.MergeStyle(form.Do), form.HeadCommitID, message, false); err != nil { 1242 if models.IsErrInvalidMergeStyle(err) { 1243 ctx.JSONError(ctx.Tr("repo.pulls.invalid_merge_option")) 1244 } else if models.IsErrMergeConflicts(err) { 1245 conflictError := err.(models.ErrMergeConflicts) 1246 flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{ 1247 "Message": ctx.Tr("repo.editor.merge_conflict"), 1248 "Summary": ctx.Tr("repo.editor.merge_conflict_summary"), 1249 "Details": utils.SanitizeFlashErrorString(conflictError.StdErr) + "<br>" + utils.SanitizeFlashErrorString(conflictError.StdOut), 1250 }) 1251 if err != nil { 1252 ctx.ServerError("MergePullRequest.HTMLString", err) 1253 return 1254 } 1255 ctx.Flash.Error(flashError) 1256 ctx.JSONRedirect(issue.Link()) 1257 } else if models.IsErrRebaseConflicts(err) { 1258 conflictError := err.(models.ErrRebaseConflicts) 1259 flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{ 1260 "Message": ctx.Tr("repo.pulls.rebase_conflict", utils.SanitizeFlashErrorString(conflictError.CommitSHA)), 1261 "Summary": ctx.Tr("repo.pulls.rebase_conflict_summary"), 1262 "Details": utils.SanitizeFlashErrorString(conflictError.StdErr) + "<br>" + utils.SanitizeFlashErrorString(conflictError.StdOut), 1263 }) 1264 if err != nil { 1265 ctx.ServerError("MergePullRequest.HTMLString", err) 1266 return 1267 } 1268 ctx.Flash.Error(flashError) 1269 ctx.JSONRedirect(issue.Link()) 1270 } else if models.IsErrMergeUnrelatedHistories(err) { 1271 log.Debug("MergeUnrelatedHistories error: %v", err) 1272 ctx.Flash.Error(ctx.Tr("repo.pulls.unrelated_histories")) 1273 ctx.JSONRedirect(issue.Link()) 1274 } else if git.IsErrPushOutOfDate(err) { 1275 log.Debug("MergePushOutOfDate error: %v", err) 1276 ctx.Flash.Error(ctx.Tr("repo.pulls.merge_out_of_date")) 1277 ctx.JSONRedirect(issue.Link()) 1278 } else if models.IsErrSHADoesNotMatch(err) { 1279 log.Debug("MergeHeadOutOfDate error: %v", err) 1280 ctx.Flash.Error(ctx.Tr("repo.pulls.head_out_of_date")) 1281 ctx.JSONRedirect(issue.Link()) 1282 } else if git.IsErrPushRejected(err) { 1283 log.Debug("MergePushRejected error: %v", err) 1284 pushrejErr := err.(*git.ErrPushRejected) 1285 message := pushrejErr.Message 1286 if len(message) == 0 { 1287 ctx.Flash.Error(ctx.Tr("repo.pulls.push_rejected_no_message")) 1288 } else { 1289 flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{ 1290 "Message": ctx.Tr("repo.pulls.push_rejected"), 1291 "Summary": ctx.Tr("repo.pulls.push_rejected_summary"), 1292 "Details": utils.SanitizeFlashErrorString(pushrejErr.Message), 1293 }) 1294 if err != nil { 1295 ctx.ServerError("MergePullRequest.HTMLString", err) 1296 return 1297 } 1298 ctx.Flash.Error(flashError) 1299 } 1300 ctx.JSONRedirect(issue.Link()) 1301 } else { 1302 ctx.ServerError("Merge", err) 1303 } 1304 return 1305 } 1306 log.Trace("Pull request merged: %d", pr.ID) 1307 1308 if err := stopTimerIfAvailable(ctx, ctx.Doer, issue); err != nil { 1309 ctx.ServerError("stopTimerIfAvailable", err) 1310 return 1311 } 1312 1313 log.Trace("Pull request merged: %d", pr.ID) 1314 1315 if form.DeleteBranchAfterMerge { 1316 // Don't cleanup when other pr use this branch as head branch 1317 exist, err := issues_model.HasUnmergedPullRequestsByHeadInfo(ctx, pr.HeadRepoID, pr.HeadBranch) 1318 if err != nil { 1319 ctx.ServerError("HasUnmergedPullRequestsByHeadInfo", err) 1320 return 1321 } 1322 if exist { 1323 ctx.JSONRedirect(issue.Link()) 1324 return 1325 } 1326 1327 var headRepo *git.Repository 1328 if ctx.Repo != nil && ctx.Repo.Repository != nil && pr.HeadRepoID == ctx.Repo.Repository.ID && ctx.Repo.GitRepo != nil { 1329 headRepo = ctx.Repo.GitRepo 1330 } else { 1331 headRepo, err = git.OpenRepository(ctx, pr.HeadRepo.RepoPath()) 1332 if err != nil { 1333 ctx.ServerError(fmt.Sprintf("OpenRepository[%s]", pr.HeadRepo.RepoPath()), err) 1334 return 1335 } 1336 defer headRepo.Close() 1337 } 1338 deleteBranch(ctx, pr, headRepo) 1339 } 1340 1341 ctx.JSONRedirect(issue.Link()) 1342 } 1343 1344 // CancelAutoMergePullRequest cancels a scheduled pr 1345 func CancelAutoMergePullRequest(ctx *context.Context) { 1346 issue, ok := getPullInfo(ctx) 1347 if !ok { 1348 return 1349 } 1350 1351 if err := automerge.RemoveScheduledAutoMerge(ctx, ctx.Doer, issue.PullRequest); err != nil { 1352 if db.IsErrNotExist(err) { 1353 ctx.Flash.Error(ctx.Tr("repo.pulls.auto_merge_not_scheduled")) 1354 ctx.Redirect(fmt.Sprintf("%s/pulls/%d", ctx.Repo.RepoLink, issue.Index)) 1355 return 1356 } 1357 ctx.ServerError("RemoveScheduledAutoMerge", err) 1358 return 1359 } 1360 ctx.Flash.Success(ctx.Tr("repo.pulls.auto_merge_canceled_schedule")) 1361 ctx.Redirect(fmt.Sprintf("%s/pulls/%d", ctx.Repo.RepoLink, issue.Index)) 1362 } 1363 1364 func stopTimerIfAvailable(ctx *context.Context, user *user_model.User, issue *issues_model.Issue) error { 1365 if issues_model.StopwatchExists(ctx, user.ID, issue.ID) { 1366 if err := issues_model.CreateOrStopIssueStopwatch(ctx, user, issue); err != nil { 1367 return err 1368 } 1369 } 1370 1371 return nil 1372 } 1373 1374 // CompareAndPullRequestPost response for creating pull request 1375 func CompareAndPullRequestPost(ctx *context.Context) { 1376 form := web.GetForm(ctx).(*forms.CreateIssueForm) 1377 ctx.Data["Title"] = ctx.Tr("repo.pulls.compare_changes") 1378 ctx.Data["PageIsComparePull"] = true 1379 ctx.Data["IsDiffCompare"] = true 1380 ctx.Data["IsRepoToolbarCommits"] = true 1381 ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes 1382 ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled 1383 upload.AddUploadContext(ctx, "comment") 1384 ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.CanWrite(unit.TypePullRequests) 1385 1386 var ( 1387 repo = ctx.Repo.Repository 1388 attachments []string 1389 ) 1390 1391 ci := ParseCompareInfo(ctx) 1392 defer func() { 1393 if ci != nil && ci.HeadGitRepo != nil { 1394 ci.HeadGitRepo.Close() 1395 } 1396 }() 1397 if ctx.Written() { 1398 return 1399 } 1400 1401 labelIDs, assigneeIDs, milestoneID, projectID := ValidateRepoMetas(ctx, *form, true) 1402 if ctx.Written() { 1403 return 1404 } 1405 1406 if setting.Attachment.Enabled { 1407 attachments = form.Files 1408 } 1409 1410 if ctx.HasError() { 1411 ctx.JSONError(ctx.GetErrMsg()) 1412 return 1413 } 1414 1415 if util.IsEmptyString(form.Title) { 1416 ctx.JSONError(ctx.Tr("repo.issues.new.title_empty")) 1417 return 1418 } 1419 1420 content := form.Content 1421 if filename := ctx.Req.Form.Get("template-file"); filename != "" { 1422 if template, err := issue_template.UnmarshalFromRepo(ctx.Repo.GitRepo, ctx.Repo.Repository.DefaultBranch, filename); err == nil { 1423 content = issue_template.RenderToMarkdown(template, ctx.Req.Form) 1424 } 1425 } 1426 1427 pullIssue := &issues_model.Issue{ 1428 RepoID: repo.ID, 1429 Repo: repo, 1430 Title: form.Title, 1431 PosterID: ctx.Doer.ID, 1432 Poster: ctx.Doer, 1433 MilestoneID: milestoneID, 1434 IsPull: true, 1435 Content: content, 1436 } 1437 pullRequest := &issues_model.PullRequest{ 1438 HeadRepoID: ci.HeadRepo.ID, 1439 BaseRepoID: repo.ID, 1440 HeadBranch: ci.HeadBranch, 1441 BaseBranch: ci.BaseBranch, 1442 HeadRepo: ci.HeadRepo, 1443 BaseRepo: repo, 1444 MergeBase: ci.CompareInfo.MergeBase, 1445 Type: issues_model.PullRequestGitea, 1446 AllowMaintainerEdit: form.AllowMaintainerEdit, 1447 } 1448 // FIXME: check error in the case two people send pull request at almost same time, give nice error prompt 1449 // instead of 500. 1450 1451 if err := pull_service.NewPullRequest(ctx, repo, pullIssue, labelIDs, attachments, pullRequest, assigneeIDs); err != nil { 1452 if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) { 1453 ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err.Error()) 1454 return 1455 } else if git.IsErrPushRejected(err) { 1456 pushrejErr := err.(*git.ErrPushRejected) 1457 message := pushrejErr.Message 1458 if len(message) == 0 { 1459 ctx.JSONError(ctx.Tr("repo.pulls.push_rejected_no_message")) 1460 return 1461 } 1462 flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{ 1463 "Message": ctx.Tr("repo.pulls.push_rejected"), 1464 "Summary": ctx.Tr("repo.pulls.push_rejected_summary"), 1465 "Details": utils.SanitizeFlashErrorString(pushrejErr.Message), 1466 }) 1467 if err != nil { 1468 ctx.ServerError("CompareAndPullRequest.HTMLString", err) 1469 return 1470 } 1471 ctx.Flash.Error(flashError) 1472 ctx.JSONRedirect(pullIssue.Link()) // FIXME: it's unfriendly, and will make the content lost 1473 return 1474 } 1475 ctx.ServerError("NewPullRequest", err) 1476 return 1477 } 1478 1479 if projectID > 0 { 1480 if !ctx.Repo.CanWrite(unit.TypeProjects) { 1481 ctx.Error(http.StatusBadRequest, "user hasn't the permission to write to projects") 1482 return 1483 } 1484 if err := issues_model.ChangeProjectAssign(pullIssue, ctx.Doer, projectID); err != nil { 1485 ctx.ServerError("ChangeProjectAssign", err) 1486 return 1487 } 1488 } 1489 1490 log.Trace("Pull request created: %d/%d", repo.ID, pullIssue.ID) 1491 ctx.JSONRedirect(pullIssue.Link()) 1492 } 1493 1494 // CleanUpPullRequest responses for delete merged branch when PR has been merged 1495 func CleanUpPullRequest(ctx *context.Context) { 1496 issue, ok := getPullInfo(ctx) 1497 if !ok { 1498 return 1499 } 1500 1501 pr := issue.PullRequest 1502 1503 // Don't cleanup unmerged and unclosed PRs 1504 if !pr.HasMerged && !issue.IsClosed { 1505 ctx.NotFound("CleanUpPullRequest", nil) 1506 return 1507 } 1508 1509 // Don't cleanup when there are other PR's that use this branch as head branch. 1510 exist, err := issues_model.HasUnmergedPullRequestsByHeadInfo(ctx, pr.HeadRepoID, pr.HeadBranch) 1511 if err != nil { 1512 ctx.ServerError("HasUnmergedPullRequestsByHeadInfo", err) 1513 return 1514 } 1515 if exist { 1516 ctx.NotFound("CleanUpPullRequest", nil) 1517 return 1518 } 1519 1520 if err := pr.LoadHeadRepo(ctx); err != nil { 1521 ctx.ServerError("LoadHeadRepo", err) 1522 return 1523 } else if pr.HeadRepo == nil { 1524 // Forked repository has already been deleted 1525 ctx.NotFound("CleanUpPullRequest", nil) 1526 return 1527 } else if err = pr.LoadBaseRepo(ctx); err != nil { 1528 ctx.ServerError("LoadBaseRepo", err) 1529 return 1530 } else if err = pr.HeadRepo.LoadOwner(ctx); err != nil { 1531 ctx.ServerError("HeadRepo.LoadOwner", err) 1532 return 1533 } 1534 1535 perm, err := access_model.GetUserRepoPermission(ctx, pr.HeadRepo, ctx.Doer) 1536 if err != nil { 1537 ctx.ServerError("GetUserRepoPermission", err) 1538 return 1539 } 1540 if !perm.CanWrite(unit.TypeCode) { 1541 ctx.NotFound("CleanUpPullRequest", nil) 1542 return 1543 } 1544 1545 fullBranchName := pr.HeadRepo.Owner.Name + "/" + pr.HeadBranch 1546 1547 var gitBaseRepo *git.Repository 1548 1549 // Assume that the base repo is the current context (almost certainly) 1550 if ctx.Repo != nil && ctx.Repo.Repository != nil && ctx.Repo.Repository.ID == pr.BaseRepoID && ctx.Repo.GitRepo != nil { 1551 gitBaseRepo = ctx.Repo.GitRepo 1552 } else { 1553 // If not just open it 1554 gitBaseRepo, err = git.OpenRepository(ctx, pr.BaseRepo.RepoPath()) 1555 if err != nil { 1556 ctx.ServerError(fmt.Sprintf("OpenRepository[%s]", pr.BaseRepo.RepoPath()), err) 1557 return 1558 } 1559 defer gitBaseRepo.Close() 1560 } 1561 1562 // Now assume that the head repo is the same as the base repo (reasonable chance) 1563 gitRepo := gitBaseRepo 1564 // But if not: is it the same as the context? 1565 if pr.BaseRepoID != pr.HeadRepoID && ctx.Repo != nil && ctx.Repo.Repository != nil && ctx.Repo.Repository.ID == pr.HeadRepoID && ctx.Repo.GitRepo != nil { 1566 gitRepo = ctx.Repo.GitRepo 1567 } else if pr.BaseRepoID != pr.HeadRepoID { 1568 // Otherwise just load it up 1569 gitRepo, err = git.OpenRepository(ctx, pr.HeadRepo.RepoPath()) 1570 if err != nil { 1571 ctx.ServerError(fmt.Sprintf("OpenRepository[%s]", pr.HeadRepo.RepoPath()), err) 1572 return 1573 } 1574 defer gitRepo.Close() 1575 } 1576 1577 defer func() { 1578 ctx.JSONRedirect(issue.Link()) 1579 }() 1580 1581 // Check if branch has no new commits 1582 headCommitID, err := gitBaseRepo.GetRefCommitID(pr.GetGitRefName()) 1583 if err != nil { 1584 log.Error("GetRefCommitID: %v", err) 1585 ctx.Flash.Error(ctx.Tr("repo.branch.deletion_failed", fullBranchName)) 1586 return 1587 } 1588 branchCommitID, err := gitRepo.GetBranchCommitID(pr.HeadBranch) 1589 if err != nil { 1590 log.Error("GetBranchCommitID: %v", err) 1591 ctx.Flash.Error(ctx.Tr("repo.branch.deletion_failed", fullBranchName)) 1592 return 1593 } 1594 if headCommitID != branchCommitID { 1595 ctx.Flash.Error(ctx.Tr("repo.branch.delete_branch_has_new_commits", fullBranchName)) 1596 return 1597 } 1598 1599 deleteBranch(ctx, pr, gitRepo) 1600 } 1601 1602 func deleteBranch(ctx *context.Context, pr *issues_model.PullRequest, gitRepo *git.Repository) { 1603 fullBranchName := pr.HeadRepo.FullName() + ":" + pr.HeadBranch 1604 if err := repo_service.DeleteBranch(ctx, ctx.Doer, pr.HeadRepo, gitRepo, pr.HeadBranch); err != nil { 1605 switch { 1606 case git.IsErrBranchNotExist(err): 1607 ctx.Flash.Error(ctx.Tr("repo.branch.deletion_failed", fullBranchName)) 1608 case errors.Is(err, repo_service.ErrBranchIsDefault): 1609 ctx.Flash.Error(ctx.Tr("repo.branch.deletion_failed", fullBranchName)) 1610 case errors.Is(err, git_model.ErrBranchIsProtected): 1611 ctx.Flash.Error(ctx.Tr("repo.branch.deletion_failed", fullBranchName)) 1612 default: 1613 log.Error("DeleteBranch: %v", err) 1614 ctx.Flash.Error(ctx.Tr("repo.branch.deletion_failed", fullBranchName)) 1615 } 1616 return 1617 } 1618 1619 if err := issues_model.AddDeletePRBranchComment(ctx, ctx.Doer, pr.BaseRepo, pr.IssueID, pr.HeadBranch); err != nil { 1620 // Do not fail here as branch has already been deleted 1621 log.Error("DeleteBranch: %v", err) 1622 } 1623 1624 ctx.Flash.Success(ctx.Tr("repo.branch.deletion_success", fullBranchName)) 1625 } 1626 1627 // DownloadPullDiff render a pull's raw diff 1628 func DownloadPullDiff(ctx *context.Context) { 1629 DownloadPullDiffOrPatch(ctx, false) 1630 } 1631 1632 // DownloadPullPatch render a pull's raw patch 1633 func DownloadPullPatch(ctx *context.Context) { 1634 DownloadPullDiffOrPatch(ctx, true) 1635 } 1636 1637 // DownloadPullDiffOrPatch render a pull's raw diff or patch 1638 func DownloadPullDiffOrPatch(ctx *context.Context, patch bool) { 1639 pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) 1640 if err != nil { 1641 if issues_model.IsErrPullRequestNotExist(err) { 1642 ctx.NotFound("GetPullRequestByIndex", err) 1643 } else { 1644 ctx.ServerError("GetPullRequestByIndex", err) 1645 } 1646 return 1647 } 1648 1649 binary := ctx.FormBool("binary") 1650 1651 if err := pull_service.DownloadDiffOrPatch(ctx, pr, ctx, patch, binary); err != nil { 1652 ctx.ServerError("DownloadDiffOrPatch", err) 1653 return 1654 } 1655 } 1656 1657 // UpdatePullRequestTarget change pull request's target branch 1658 func UpdatePullRequestTarget(ctx *context.Context) { 1659 issue := GetActionIssue(ctx) 1660 if ctx.Written() { 1661 return 1662 } 1663 pr := issue.PullRequest 1664 if !issue.IsPull { 1665 ctx.Error(http.StatusNotFound) 1666 return 1667 } 1668 1669 if !ctx.IsSigned || (!issue.IsPoster(ctx.Doer.ID) && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)) { 1670 ctx.Error(http.StatusForbidden) 1671 return 1672 } 1673 1674 targetBranch := ctx.FormTrim("target_branch") 1675 if len(targetBranch) == 0 { 1676 ctx.Error(http.StatusNoContent) 1677 return 1678 } 1679 1680 if err := pull_service.ChangeTargetBranch(ctx, pr, ctx.Doer, targetBranch); err != nil { 1681 if issues_model.IsErrPullRequestAlreadyExists(err) { 1682 err := err.(issues_model.ErrPullRequestAlreadyExists) 1683 1684 RepoRelPath := ctx.Repo.Owner.Name + "/" + ctx.Repo.Repository.Name 1685 errorMessage := ctx.Tr("repo.pulls.has_pull_request", html.EscapeString(ctx.Repo.RepoLink+"/pulls/"+strconv.FormatInt(err.IssueID, 10)), html.EscapeString(RepoRelPath), err.IssueID) // FIXME: Creates url inside locale string 1686 1687 ctx.Flash.Error(errorMessage) 1688 ctx.JSON(http.StatusConflict, map[string]any{ 1689 "error": err.Error(), 1690 "user_error": errorMessage, 1691 }) 1692 } else if issues_model.IsErrIssueIsClosed(err) { 1693 errorMessage := ctx.Tr("repo.pulls.is_closed") 1694 1695 ctx.Flash.Error(errorMessage) 1696 ctx.JSON(http.StatusConflict, map[string]any{ 1697 "error": err.Error(), 1698 "user_error": errorMessage, 1699 }) 1700 } else if models.IsErrPullRequestHasMerged(err) { 1701 errorMessage := ctx.Tr("repo.pulls.has_merged") 1702 1703 ctx.Flash.Error(errorMessage) 1704 ctx.JSON(http.StatusConflict, map[string]any{ 1705 "error": err.Error(), 1706 "user_error": errorMessage, 1707 }) 1708 } else if git_model.IsErrBranchesEqual(err) { 1709 errorMessage := ctx.Tr("repo.pulls.nothing_to_compare") 1710 1711 ctx.Flash.Error(errorMessage) 1712 ctx.JSON(http.StatusBadRequest, map[string]any{ 1713 "error": err.Error(), 1714 "user_error": errorMessage, 1715 }) 1716 } else { 1717 ctx.ServerError("UpdatePullRequestTarget", err) 1718 } 1719 return 1720 } 1721 notify_service.PullRequestChangeTargetBranch(ctx, ctx.Doer, pr, targetBranch) 1722 1723 ctx.JSON(http.StatusOK, map[string]any{ 1724 "base_branch": pr.BaseBranch, 1725 }) 1726 } 1727 1728 // SetAllowEdits allow edits from maintainers to PRs 1729 func SetAllowEdits(ctx *context.Context) { 1730 form := web.GetForm(ctx).(*forms.UpdateAllowEditsForm) 1731 1732 pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) 1733 if err != nil { 1734 if issues_model.IsErrPullRequestNotExist(err) { 1735 ctx.NotFound("GetPullRequestByIndex", err) 1736 } else { 1737 ctx.ServerError("GetPullRequestByIndex", err) 1738 } 1739 return 1740 } 1741 1742 if err := pull_service.SetAllowEdits(ctx, ctx.Doer, pr, form.AllowMaintainerEdit); err != nil { 1743 if errors.Is(pull_service.ErrUserHasNoPermissionForAction, err) { 1744 ctx.Error(http.StatusForbidden) 1745 return 1746 } 1747 ctx.ServerError("SetAllowEdits", err) 1748 return 1749 } 1750 1751 ctx.JSON(http.StatusOK, map[string]any{ 1752 "allow_maintainer_edit": pr.AllowMaintainerEdit, 1753 }) 1754 }