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  }