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  }