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