loading up the forgejo repo on tangled to test page performance
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at forgejo 562 lines 19 kB view raw
1// Copyright 2019 The Gitea Authors. 2// All rights reserved. 3// SPDX-License-Identifier: MIT 4 5package pull 6 7import ( 8 "context" 9 "fmt" 10 "net/url" 11 "os" 12 "path/filepath" 13 "regexp" 14 "strconv" 15 "strings" 16 17 "forgejo.org/models" 18 "forgejo.org/models/db" 19 git_model "forgejo.org/models/git" 20 issues_model "forgejo.org/models/issues" 21 access_model "forgejo.org/models/perm/access" 22 repo_model "forgejo.org/models/repo" 23 "forgejo.org/models/unit" 24 user_model "forgejo.org/models/user" 25 "forgejo.org/modules/cache" 26 "forgejo.org/modules/git" 27 "forgejo.org/modules/log" 28 "forgejo.org/modules/references" 29 repo_module "forgejo.org/modules/repository" 30 "forgejo.org/modules/setting" 31 "forgejo.org/modules/timeutil" 32 issue_service "forgejo.org/services/issue" 33 notify_service "forgejo.org/services/notify" 34) 35 36// getMergeMessage composes the message used when merging a pull request. 37func getMergeMessage(ctx context.Context, baseGitRepo *git.Repository, pr *issues_model.PullRequest, mergeStyle repo_model.MergeStyle, extraVars map[string]string) (message, body string, err error) { 38 if err := pr.LoadBaseRepo(ctx); err != nil { 39 return "", "", err 40 } 41 if err := pr.LoadHeadRepo(ctx); err != nil { 42 return "", "", err 43 } 44 if err := pr.LoadIssue(ctx); err != nil { 45 return "", "", err 46 } 47 if err := pr.Issue.LoadPoster(ctx); err != nil { 48 return "", "", err 49 } 50 if err := pr.Issue.LoadRepo(ctx); err != nil { 51 return "", "", err 52 } 53 54 isExternalTracker := pr.BaseRepo.UnitEnabled(ctx, unit.TypeExternalTracker) 55 issueReference := "#" 56 if isExternalTracker { 57 issueReference = "!" 58 } 59 60 issueURL, err := url.JoinPath(setting.AppURL, pr.Issue.Link()) 61 if err != nil { 62 return "", "", err 63 } 64 reviewedOn := fmt.Sprintf("Reviewed-on: %s", issueURL) 65 reviewedBy := pr.GetApprovers(ctx) 66 67 if mergeStyle != "" { 68 commit, err := baseGitRepo.GetBranchCommit(pr.BaseRepo.DefaultBranch) 69 if err != nil { 70 return "", "", err 71 } 72 73 templateFilepathForgejo := fmt.Sprintf(".forgejo/default_merge_message/%s_TEMPLATE.md", strings.ToUpper(string(mergeStyle))) 74 templateFilepathGitea := fmt.Sprintf(".gitea/default_merge_message/%s_TEMPLATE.md", strings.ToUpper(string(mergeStyle))) 75 76 templateContent, err := commit.GetFileContent(templateFilepathForgejo, setting.Repository.PullRequest.DefaultMergeMessageSize) 77 if _, ok := err.(git.ErrNotExist); ok { 78 templateContent, err = commit.GetFileContent(templateFilepathGitea, setting.Repository.PullRequest.DefaultMergeMessageSize) 79 } 80 if err != nil { 81 if !git.IsErrNotExist(err) { 82 return "", "", err 83 } 84 } else { 85 vars := map[string]string{ 86 "BaseRepoOwnerName": pr.BaseRepo.OwnerName, 87 "BaseRepoName": pr.BaseRepo.Name, 88 "BaseBranch": pr.BaseBranch, 89 "HeadRepoOwnerName": "", 90 "HeadRepoName": "", 91 "HeadBranch": pr.HeadBranch, 92 "PullRequestTitle": pr.Issue.Title, 93 "PullRequestDescription": pr.Issue.Content, 94 "PullRequestPosterName": pr.Issue.Poster.Name, 95 "PullRequestIndex": strconv.FormatInt(pr.Index, 10), 96 "PullRequestReference": fmt.Sprintf("%s%d", issueReference, pr.Index), 97 "ReviewedOn": reviewedOn, 98 "ReviewedBy": reviewedBy, 99 } 100 if pr.HeadRepo != nil { 101 vars["HeadRepoOwnerName"] = pr.HeadRepo.OwnerName 102 vars["HeadRepoName"] = pr.HeadRepo.Name 103 } 104 for extraKey, extraValue := range extraVars { 105 vars[extraKey] = extraValue 106 } 107 refs, err := pr.ResolveCrossReferences(ctx) 108 if err == nil { 109 closeIssueIndexes := make([]string, 0, len(refs)) 110 closeWord := "close" 111 if len(setting.Repository.PullRequest.CloseKeywords) > 0 { 112 closeWord = setting.Repository.PullRequest.CloseKeywords[0] 113 } 114 for _, ref := range refs { 115 if ref.RefAction == references.XRefActionCloses { 116 if err := ref.LoadIssue(ctx); err != nil { 117 return "", "", err 118 } 119 closeIssueIndexes = append(closeIssueIndexes, fmt.Sprintf("%s %s%d", closeWord, issueReference, ref.Issue.Index)) 120 } 121 } 122 if len(closeIssueIndexes) > 0 { 123 vars["ClosingIssues"] = strings.Join(closeIssueIndexes, ", ") 124 } else { 125 vars["ClosingIssues"] = "" 126 } 127 } 128 message, body = expandDefaultMergeMessage(templateContent, vars) 129 return message, body, nil 130 } 131 } 132 133 if mergeStyle == repo_model.MergeStyleRebase { 134 // for fast-forward rebase, do not amend the last commit if there is no template 135 return "", "", nil 136 } 137 138 body = fmt.Sprintf("%s\n%s", reviewedOn, reviewedBy) 139 140 // Squash merge has a different from other styles. 141 if mergeStyle == repo_model.MergeStyleSquash { 142 return fmt.Sprintf("%s (%s%d)", pr.Issue.Title, issueReference, pr.Issue.Index), body, nil 143 } 144 145 if pr.BaseRepoID == pr.HeadRepoID { 146 return fmt.Sprintf("Merge pull request '%s' (%s%d) from %s into %s", pr.Issue.Title, issueReference, pr.Issue.Index, pr.HeadBranch, pr.BaseBranch), body, nil 147 } 148 149 if pr.HeadRepo == nil { 150 return fmt.Sprintf("Merge pull request '%s' (%s%d) from <deleted>:%s into %s", pr.Issue.Title, issueReference, pr.Issue.Index, pr.HeadBranch, pr.BaseBranch), body, nil 151 } 152 153 return fmt.Sprintf("Merge pull request '%s' (%s%d) from %s:%s into %s", pr.Issue.Title, issueReference, pr.Issue.Index, pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseBranch), body, nil 154} 155 156func expandDefaultMergeMessage(template string, vars map[string]string) (message, body string) { 157 message = strings.TrimSpace(template) 158 if splits := strings.SplitN(message, "\n", 2); len(splits) == 2 { 159 message = splits[0] 160 body = strings.TrimSpace(splits[1]) 161 } 162 mapping := func(s string) string { return vars[s] } 163 return os.Expand(message, mapping), os.Expand(body, mapping) 164} 165 166// GetDefaultMergeMessage returns default message used when merging pull request 167func GetDefaultMergeMessage(ctx context.Context, baseGitRepo *git.Repository, pr *issues_model.PullRequest, mergeStyle repo_model.MergeStyle) (message, body string, err error) { 168 return getMergeMessage(ctx, baseGitRepo, pr, mergeStyle, nil) 169} 170 171// Merge merges pull request to base repository. 172// Caller should check PR is ready to be merged (review and status checks) 173func Merge(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.User, baseGitRepo *git.Repository, mergeStyle repo_model.MergeStyle, expectedHeadCommitID, message string, wasAutoMerged bool) error { 174 if err := pr.LoadBaseRepo(ctx); err != nil { 175 log.Error("Unable to load base repo: %v", err) 176 return fmt.Errorf("unable to load base repo: %w", err) 177 } else if err := pr.LoadHeadRepo(ctx); err != nil { 178 log.Error("Unable to load head repo: %v", err) 179 return fmt.Errorf("unable to load head repo: %w", err) 180 } 181 182 pullWorkingPool.CheckIn(fmt.Sprint(pr.ID)) 183 defer pullWorkingPool.CheckOut(fmt.Sprint(pr.ID)) 184 185 prUnit, err := pr.BaseRepo.GetUnit(ctx, unit.TypePullRequests) 186 if err != nil { 187 log.Error("pr.BaseRepo.GetUnit(unit.TypePullRequests): %v", err) 188 return err 189 } 190 prConfig := prUnit.PullRequestsConfig() 191 192 // Check if merge style is correct and allowed 193 if !prConfig.IsMergeStyleAllowed(mergeStyle) { 194 return models.ErrInvalidMergeStyle{ID: pr.BaseRepo.ID, Style: mergeStyle} 195 } 196 197 defer func() { 198 AddTestPullRequestTask(ctx, doer, pr.BaseRepo.ID, pr.BaseBranch, false, "", "", 0) 199 }() 200 201 _, err = doMergeAndPush(ctx, pr, doer, mergeStyle, expectedHeadCommitID, message, repo_module.PushTriggerPRMergeToBase) 202 if err != nil { 203 return err 204 } 205 206 // reload pull request because it has been updated by post receive hook 207 pr, err = issues_model.GetPullRequestByID(ctx, pr.ID) 208 if err != nil { 209 return err 210 } 211 212 if err := pr.LoadIssue(ctx); err != nil { 213 log.Error("LoadIssue %-v: %v", pr, err) 214 } 215 216 if err := pr.Issue.LoadRepo(ctx); err != nil { 217 log.Error("pr.Issue.LoadRepo %-v: %v", pr, err) 218 } 219 if err := pr.Issue.Repo.LoadOwner(ctx); err != nil { 220 log.Error("LoadOwner for %-v: %v", pr, err) 221 } 222 223 if wasAutoMerged { 224 notify_service.AutoMergePullRequest(ctx, doer, pr) 225 } else { 226 notify_service.MergePullRequest(ctx, doer, pr) 227 } 228 229 // Reset cached commit count 230 cache.Remove(pr.Issue.Repo.GetCommitsCountCacheKey(pr.BaseBranch, true)) 231 232 return handleCloseCrossReferences(ctx, pr, doer) 233} 234 235func handleCloseCrossReferences(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.User) error { 236 // Resolve cross references 237 refs, err := pr.ResolveCrossReferences(ctx) 238 if err != nil { 239 log.Error("ResolveCrossReferences: %v", err) 240 return nil 241 } 242 243 for _, ref := range refs { 244 if err = ref.LoadIssue(ctx); err != nil { 245 return err 246 } 247 if err = ref.Issue.LoadRepo(ctx); err != nil { 248 return err 249 } 250 isClosed := ref.RefAction == references.XRefActionCloses 251 if isClosed != ref.Issue.IsClosed { 252 if err = issue_service.ChangeStatus(ctx, ref.Issue, doer, pr.MergedCommitID, isClosed); err != nil { 253 // Allow ErrDependenciesLeft 254 if !issues_model.IsErrDependenciesLeft(err) { 255 return err 256 } 257 } 258 } 259 } 260 return nil 261} 262 263// doMergeAndPush performs the merge operation without changing any pull information in database and pushes it up to the base repository 264func doMergeAndPush(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.User, mergeStyle repo_model.MergeStyle, expectedHeadCommitID, message string, pushTrigger repo_module.PushTrigger) (string, error) { //nolint:unparam 265 // Clone base repo. 266 mergeCtx, cancel, err := createTemporaryRepoForMerge(ctx, pr, doer, expectedHeadCommitID) 267 if err != nil { 268 return "", err 269 } 270 defer cancel() 271 272 // Merge commits. 273 switch mergeStyle { 274 case repo_model.MergeStyleMerge: 275 if err := doMergeStyleMerge(mergeCtx, message); err != nil { 276 return "", err 277 } 278 case repo_model.MergeStyleRebase, repo_model.MergeStyleRebaseMerge: 279 if err := doMergeStyleRebase(mergeCtx, mergeStyle, message); err != nil { 280 return "", err 281 } 282 case repo_model.MergeStyleSquash: 283 if err := doMergeStyleSquash(mergeCtx, message); err != nil { 284 return "", err 285 } 286 case repo_model.MergeStyleFastForwardOnly: 287 if err := doMergeStyleFastForwardOnly(mergeCtx); err != nil { 288 return "", err 289 } 290 default: 291 return "", models.ErrInvalidMergeStyle{ID: pr.BaseRepo.ID, Style: mergeStyle} 292 } 293 294 // OK we should cache our current head and origin/headbranch 295 mergeHeadSHA, err := git.GetFullCommitID(ctx, mergeCtx.tmpBasePath, "HEAD") 296 if err != nil { 297 return "", fmt.Errorf("Failed to get full commit id for HEAD: %w", err) 298 } 299 mergeBaseSHA, err := git.GetFullCommitID(ctx, mergeCtx.tmpBasePath, "original_"+baseBranch) 300 if err != nil { 301 return "", fmt.Errorf("Failed to get full commit id for origin/%s: %w", pr.BaseBranch, err) 302 } 303 mergeCommitID, err := git.GetFullCommitID(ctx, mergeCtx.tmpBasePath, baseBranch) 304 if err != nil { 305 return "", fmt.Errorf("Failed to get full commit id for the new merge: %w", err) 306 } 307 308 // Now it's questionable about where this should go - either after or before the push 309 // I think in the interests of data safety - failures to push to the lfs should prevent 310 // the merge as you can always remerge. 311 if setting.LFS.StartServer { 312 if err := LFSPush(ctx, mergeCtx.tmpBasePath, mergeHeadSHA, mergeBaseSHA, pr); err != nil { 313 return "", err 314 } 315 } 316 317 var headUser *user_model.User 318 err = pr.HeadRepo.LoadOwner(ctx) 319 if err != nil { 320 if !user_model.IsErrUserNotExist(err) { 321 log.Error("Can't find user: %d for head repository in %-v: %v", pr.HeadRepo.OwnerID, pr, err) 322 return "", err 323 } 324 log.Warn("Can't find user: %d for head repository in %-v - defaulting to doer: %s - %v", pr.HeadRepo.OwnerID, pr, doer.Name, err) 325 headUser = doer 326 } else { 327 headUser = pr.HeadRepo.Owner 328 } 329 330 mergeCtx.env = repo_module.FullPushingEnvironment( 331 headUser, 332 doer, 333 pr.BaseRepo, 334 pr.BaseRepo.Name, 335 pr.ID, 336 ) 337 338 mergeCtx.env = append(mergeCtx.env, repo_module.EnvPushTrigger+"="+string(pushTrigger)) 339 pushCmd := git.NewCommand(ctx, "push", "origin").AddDynamicArguments(baseBranch + ":" + git.BranchPrefix + pr.BaseBranch) 340 341 // Push back to upstream. 342 // This cause an api call to "/api/internal/hook/post-receive/...", 343 // If it's merge, all db transaction and operations should be there but not here to prevent deadlock. 344 if err := pushCmd.Run(mergeCtx.RunOpts()); err != nil { 345 if strings.Contains(mergeCtx.errbuf.String(), "non-fast-forward") { 346 return "", &git.ErrPushOutOfDate{ 347 StdOut: mergeCtx.outbuf.String(), 348 StdErr: mergeCtx.errbuf.String(), 349 Err: err, 350 } 351 } else if strings.Contains(mergeCtx.errbuf.String(), "! [remote rejected]") { 352 err := &git.ErrPushRejected{ 353 StdOut: mergeCtx.outbuf.String(), 354 StdErr: mergeCtx.errbuf.String(), 355 Err: err, 356 } 357 err.GenerateMessage() 358 return "", err 359 } 360 return "", fmt.Errorf("git push: %s", mergeCtx.errbuf.String()) 361 } 362 mergeCtx.outbuf.Reset() 363 mergeCtx.errbuf.Reset() 364 365 return mergeCommitID, nil 366} 367 368func commitAndSignNoAuthor(ctx *mergeContext, message string) error { 369 cmdCommit := git.NewCommand(ctx, "commit").AddOptionFormat("--message=%s", message) 370 if ctx.signKeyID == "" { 371 cmdCommit.AddArguments("--no-gpg-sign") 372 } else { 373 cmdCommit.AddOptionFormat("-S%s", ctx.signKeyID) 374 } 375 if err := cmdCommit.Run(ctx.RunOpts()); err != nil { 376 log.Error("git commit %-v: %v\n%s\n%s", ctx.pr, err, ctx.outbuf.String(), ctx.errbuf.String()) 377 return fmt.Errorf("git commit %v: %w\n%s\n%s", ctx.pr, err, ctx.outbuf.String(), ctx.errbuf.String()) 378 } 379 return nil 380} 381 382func runMergeCommand(ctx *mergeContext, mergeStyle repo_model.MergeStyle, cmd *git.Command) error { 383 if err := cmd.Run(ctx.RunOpts()); err != nil { 384 // Merge will leave a MERGE_HEAD file in the .git folder if there is a conflict 385 if _, statErr := os.Stat(filepath.Join(ctx.tmpBasePath, ".git", "MERGE_HEAD")); statErr == nil { 386 // We have a merge conflict error 387 log.Debug("MergeConflict %-v: %v\n%s\n%s", ctx.pr, err, ctx.outbuf.String(), ctx.errbuf.String()) 388 return models.ErrMergeConflicts{ 389 Style: mergeStyle, 390 StdOut: ctx.outbuf.String(), 391 StdErr: ctx.errbuf.String(), 392 Err: err, 393 } 394 } else if strings.Contains(ctx.errbuf.String(), "refusing to merge unrelated histories") { 395 log.Debug("MergeUnrelatedHistories %-v: %v\n%s\n%s", ctx.pr, err, ctx.outbuf.String(), ctx.errbuf.String()) 396 return models.ErrMergeUnrelatedHistories{ 397 Style: mergeStyle, 398 StdOut: ctx.outbuf.String(), 399 StdErr: ctx.errbuf.String(), 400 Err: err, 401 } 402 } else if mergeStyle == repo_model.MergeStyleFastForwardOnly && strings.Contains(ctx.errbuf.String(), "Not possible to fast-forward, aborting") { 403 log.Debug("MergeDivergingFastForwardOnly %-v: %v\n%s\n%s", ctx.pr, err, ctx.outbuf.String(), ctx.errbuf.String()) 404 return models.ErrMergeDivergingFastForwardOnly{ 405 StdOut: ctx.outbuf.String(), 406 StdErr: ctx.errbuf.String(), 407 Err: err, 408 } 409 } 410 log.Error("git merge %-v: %v\n%s\n%s", ctx.pr, err, ctx.outbuf.String(), ctx.errbuf.String()) 411 return fmt.Errorf("git merge %v: %w\n%s\n%s", ctx.pr, err, ctx.outbuf.String(), ctx.errbuf.String()) 412 } 413 ctx.outbuf.Reset() 414 ctx.errbuf.Reset() 415 416 return nil 417} 418 419var escapedSymbols = regexp.MustCompile(`([*[?! \\])`) 420 421// IsUserAllowedToMerge check if user is allowed to merge PR with given permissions and branch protections 422func IsUserAllowedToMerge(ctx context.Context, pr *issues_model.PullRequest, p access_model.Permission, user *user_model.User) (bool, error) { 423 if user == nil { 424 return false, nil 425 } 426 427 pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepoID, pr.BaseBranch) 428 if err != nil { 429 return false, err 430 } 431 432 if (p.CanWrite(unit.TypeCode) && pb == nil) || (pb != nil && git_model.IsUserMergeWhitelisted(ctx, pb, user.ID, p)) { 433 return true, nil 434 } 435 436 return false, nil 437} 438 439// CheckPullBranchProtections checks whether the PR is ready to be merged (reviews and status checks). 440// Returns the protected branch rule when `ErrDisallowedToMerge` is returned as error. 441func CheckPullBranchProtections(ctx context.Context, pr *issues_model.PullRequest, skipProtectedFilesCheck bool) (protectedBranchRule *git_model.ProtectedBranch, err error) { 442 if err = pr.LoadBaseRepo(ctx); err != nil { 443 return nil, fmt.Errorf("LoadBaseRepo: %w", err) 444 } 445 446 pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepoID, pr.BaseBranch) 447 if err != nil { 448 return nil, fmt.Errorf("LoadProtectedBranch: %v", err) 449 } 450 if pb == nil { 451 return nil, nil 452 } 453 454 isPass, err := IsPullCommitStatusPass(ctx, pr) 455 if err != nil { 456 return nil, err 457 } 458 if !isPass { 459 return pb, models.ErrDisallowedToMerge{ 460 Reason: "Not all required status checks successful", 461 } 462 } 463 464 if !issues_model.HasEnoughApprovals(ctx, pb, pr) { 465 return pb, models.ErrDisallowedToMerge{ 466 Reason: "Does not have enough approvals", 467 } 468 } 469 if issues_model.MergeBlockedByRejectedReview(ctx, pb, pr) { 470 return pb, models.ErrDisallowedToMerge{ 471 Reason: "There are requested changes", 472 } 473 } 474 if issues_model.MergeBlockedByOfficialReviewRequests(ctx, pb, pr) { 475 return pb, models.ErrDisallowedToMerge{ 476 Reason: "There are official review requests", 477 } 478 } 479 480 if issues_model.MergeBlockedByOutdatedBranch(pb, pr) { 481 return pb, models.ErrDisallowedToMerge{ 482 Reason: "The head branch is behind the base branch", 483 } 484 } 485 486 if skipProtectedFilesCheck { 487 return nil, nil 488 } 489 490 if pb.MergeBlockedByProtectedFiles(pr.ChangedProtectedFiles) { 491 return pb, models.ErrDisallowedToMerge{ 492 Reason: "Changed protected files", 493 } 494 } 495 496 return nil, nil 497} 498 499// MergedManually mark pr as merged manually 500func MergedManually(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.User, baseGitRepo *git.Repository, commitID string) error { 501 pullWorkingPool.CheckIn(fmt.Sprint(pr.ID)) 502 defer pullWorkingPool.CheckOut(fmt.Sprint(pr.ID)) 503 504 if err := db.WithTx(ctx, func(ctx context.Context) error { 505 if err := pr.LoadBaseRepo(ctx); err != nil { 506 return err 507 } 508 prUnit, err := pr.BaseRepo.GetUnit(ctx, unit.TypePullRequests) 509 if err != nil { 510 return err 511 } 512 prConfig := prUnit.PullRequestsConfig() 513 514 // Check if merge style is correct and allowed 515 if !prConfig.IsMergeStyleAllowed(repo_model.MergeStyleManuallyMerged) { 516 return models.ErrInvalidMergeStyle{ID: pr.BaseRepo.ID, Style: repo_model.MergeStyleManuallyMerged} 517 } 518 519 objectFormat := git.ObjectFormatFromName(pr.BaseRepo.ObjectFormatName) 520 if len(commitID) != objectFormat.FullLength() { 521 return fmt.Errorf("Wrong commit ID") 522 } 523 524 commit, err := baseGitRepo.GetCommit(commitID) 525 if err != nil { 526 if git.IsErrNotExist(err) { 527 return fmt.Errorf("Wrong commit ID") 528 } 529 return err 530 } 531 commitID = commit.ID.String() 532 533 ok, err := baseGitRepo.IsCommitInBranch(commitID, pr.BaseBranch) 534 if err != nil { 535 return err 536 } 537 if !ok { 538 return fmt.Errorf("Wrong commit ID") 539 } 540 541 pr.MergedCommitID = commitID 542 pr.MergedUnix = timeutil.TimeStamp(commit.Author.When.Unix()) 543 pr.Status = issues_model.PullRequestStatusManuallyMerged 544 pr.Merger = doer 545 pr.MergerID = doer.ID 546 547 var merged bool 548 if merged, err = pr.SetMerged(ctx); err != nil { 549 return err 550 } else if !merged { 551 return fmt.Errorf("SetMerged failed") 552 } 553 return nil 554 }); err != nil { 555 return err 556 } 557 558 notify_service.MergePullRequest(baseGitRepo.Ctx, doer, pr) 559 log.Info("manuallyMerged[%d]: Marked as manually merged into %s/%s by commit id: %s", pr.ID, pr.BaseRepo.Name, pr.BaseBranch, commitID) 560 561 return handleCloseCrossReferences(ctx, pr, doer) 562}