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