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