code.gitea.io/gitea@v1.21.7/services/pull/pull.go (about)

     1  // Copyright 2019 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package pull
     5  
     6  import (
     7  	"bytes"
     8  	"context"
     9  	"fmt"
    10  	"io"
    11  	"os"
    12  	"regexp"
    13  	"strings"
    14  	"time"
    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  	repo_model "code.gitea.io/gitea/models/repo"
    21  	user_model "code.gitea.io/gitea/models/user"
    22  	"code.gitea.io/gitea/modules/base"
    23  	"code.gitea.io/gitea/modules/container"
    24  	gitea_context "code.gitea.io/gitea/modules/context"
    25  	"code.gitea.io/gitea/modules/git"
    26  	"code.gitea.io/gitea/modules/graceful"
    27  	"code.gitea.io/gitea/modules/json"
    28  	"code.gitea.io/gitea/modules/log"
    29  	repo_module "code.gitea.io/gitea/modules/repository"
    30  	"code.gitea.io/gitea/modules/setting"
    31  	"code.gitea.io/gitea/modules/sync"
    32  	"code.gitea.io/gitea/modules/util"
    33  	issue_service "code.gitea.io/gitea/services/issue"
    34  	notify_service "code.gitea.io/gitea/services/notify"
    35  )
    36  
    37  // TODO: use clustered lock (unique queue? or *abuse* cache)
    38  var pullWorkingPool = sync.NewExclusivePool()
    39  
    40  // NewPullRequest creates new pull request with labels for repository.
    41  func NewPullRequest(ctx context.Context, repo *repo_model.Repository, issue *issues_model.Issue, labelIDs []int64, uuids []string, pr *issues_model.PullRequest, assigneeIDs []int64) error {
    42  	prCtx, cancel, err := createTemporaryRepoForPR(ctx, pr)
    43  	if err != nil {
    44  		if !git_model.IsErrBranchNotExist(err) {
    45  			log.Error("CreateTemporaryRepoForPR %-v: %v", pr, err)
    46  		}
    47  		return err
    48  	}
    49  	defer cancel()
    50  
    51  	if err := testPatch(ctx, prCtx, pr); err != nil {
    52  		return err
    53  	}
    54  
    55  	divergence, err := git.GetDivergingCommits(ctx, prCtx.tmpBasePath, baseBranch, trackingBranch)
    56  	if err != nil {
    57  		return err
    58  	}
    59  	pr.CommitsAhead = divergence.Ahead
    60  	pr.CommitsBehind = divergence.Behind
    61  
    62  	assigneeCommentMap := make(map[int64]*issues_model.Comment)
    63  
    64  	// add first push codes comment
    65  	baseGitRepo, err := git.OpenRepository(ctx, pr.BaseRepo.RepoPath())
    66  	if err != nil {
    67  		return err
    68  	}
    69  	defer baseGitRepo.Close()
    70  
    71  	if err := db.WithTx(ctx, func(ctx context.Context) error {
    72  		if err := issues_model.NewPullRequest(ctx, repo, issue, labelIDs, uuids, pr); err != nil {
    73  			return err
    74  		}
    75  
    76  		for _, assigneeID := range assigneeIDs {
    77  			comment, err := issue_service.AddAssigneeIfNotAssigned(ctx, issue, issue.Poster, assigneeID, false)
    78  			if err != nil {
    79  				return err
    80  			}
    81  			assigneeCommentMap[assigneeID] = comment
    82  		}
    83  
    84  		pr.Issue = issue
    85  		issue.PullRequest = pr
    86  
    87  		if pr.Flow == issues_model.PullRequestFlowGithub {
    88  			err = PushToBaseRepo(ctx, pr)
    89  		} else {
    90  			err = UpdateRef(ctx, pr)
    91  		}
    92  		if err != nil {
    93  			return err
    94  		}
    95  
    96  		compareInfo, err := baseGitRepo.GetCompareInfo(pr.BaseRepo.RepoPath(),
    97  			git.BranchPrefix+pr.BaseBranch, pr.GetGitRefName(), false, false)
    98  		if err != nil {
    99  			return err
   100  		}
   101  		if len(compareInfo.Commits) == 0 {
   102  			return nil
   103  		}
   104  
   105  		data := issues_model.PushActionContent{IsForcePush: false}
   106  		data.CommitIDs = make([]string, 0, len(compareInfo.Commits))
   107  		for i := len(compareInfo.Commits) - 1; i >= 0; i-- {
   108  			data.CommitIDs = append(data.CommitIDs, compareInfo.Commits[i].ID.String())
   109  		}
   110  
   111  		dataJSON, err := json.Marshal(data)
   112  		if err != nil {
   113  			return err
   114  		}
   115  
   116  		ops := &issues_model.CreateCommentOptions{
   117  			Type:        issues_model.CommentTypePullRequestPush,
   118  			Doer:        issue.Poster,
   119  			Repo:        repo,
   120  			Issue:       pr.Issue,
   121  			IsForcePush: false,
   122  			Content:     string(dataJSON),
   123  		}
   124  
   125  		if _, err = issues_model.CreateComment(ctx, ops); err != nil {
   126  			return err
   127  		}
   128  
   129  		if !pr.IsWorkInProgress() {
   130  			if err := issues_model.PullRequestCodeOwnersReview(ctx, issue, pr); err != nil {
   131  				return err
   132  			}
   133  		}
   134  		return nil
   135  	}); err != nil {
   136  		// cleanup: this will only remove the reference, the real commit will be clean up when next GC
   137  		if err1 := baseGitRepo.RemoveReference(pr.GetGitRefName()); err1 != nil {
   138  			log.Error("RemoveReference: %v", err1)
   139  		}
   140  		return err
   141  	}
   142  	baseGitRepo.Close() // close immediately to avoid notifications will open the repository again
   143  
   144  	mentions, err := issues_model.FindAndUpdateIssueMentions(ctx, issue, issue.Poster, issue.Content)
   145  	if err != nil {
   146  		return err
   147  	}
   148  
   149  	notify_service.NewPullRequest(ctx, pr, mentions)
   150  	if len(issue.Labels) > 0 {
   151  		notify_service.IssueChangeLabels(ctx, issue.Poster, issue, issue.Labels, nil)
   152  	}
   153  	if issue.Milestone != nil {
   154  		notify_service.IssueChangeMilestone(ctx, issue.Poster, issue, 0)
   155  	}
   156  	if len(assigneeIDs) > 0 {
   157  		for _, assigneeID := range assigneeIDs {
   158  			assignee, err := user_model.GetUserByID(ctx, assigneeID)
   159  			if err != nil {
   160  				return ErrDependenciesLeft
   161  			}
   162  			notify_service.IssueChangeAssignee(ctx, issue.Poster, issue, assignee, false, assigneeCommentMap[assigneeID])
   163  		}
   164  	}
   165  
   166  	return nil
   167  }
   168  
   169  // ChangeTargetBranch changes the target branch of this pull request, as the given user.
   170  func ChangeTargetBranch(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.User, targetBranch string) (err error) {
   171  	pullWorkingPool.CheckIn(fmt.Sprint(pr.ID))
   172  	defer pullWorkingPool.CheckOut(fmt.Sprint(pr.ID))
   173  
   174  	// Current target branch is already the same
   175  	if pr.BaseBranch == targetBranch {
   176  		return nil
   177  	}
   178  
   179  	if pr.Issue.IsClosed {
   180  		return issues_model.ErrIssueIsClosed{
   181  			ID:     pr.Issue.ID,
   182  			RepoID: pr.Issue.RepoID,
   183  			Index:  pr.Issue.Index,
   184  		}
   185  	}
   186  
   187  	if pr.HasMerged {
   188  		return models.ErrPullRequestHasMerged{
   189  			ID:         pr.ID,
   190  			IssueID:    pr.Index,
   191  			HeadRepoID: pr.HeadRepoID,
   192  			BaseRepoID: pr.BaseRepoID,
   193  			HeadBranch: pr.HeadBranch,
   194  			BaseBranch: pr.BaseBranch,
   195  		}
   196  	}
   197  
   198  	// Check if branches are equal
   199  	branchesEqual, err := IsHeadEqualWithBranch(ctx, pr, targetBranch)
   200  	if err != nil {
   201  		return err
   202  	}
   203  	if branchesEqual {
   204  		return git_model.ErrBranchesEqual{
   205  			HeadBranchName: pr.HeadBranch,
   206  			BaseBranchName: targetBranch,
   207  		}
   208  	}
   209  
   210  	// Check if pull request for the new target branch already exists
   211  	existingPr, err := issues_model.GetUnmergedPullRequest(ctx, pr.HeadRepoID, pr.BaseRepoID, pr.HeadBranch, targetBranch, issues_model.PullRequestFlowGithub)
   212  	if existingPr != nil {
   213  		return issues_model.ErrPullRequestAlreadyExists{
   214  			ID:         existingPr.ID,
   215  			IssueID:    existingPr.Index,
   216  			HeadRepoID: existingPr.HeadRepoID,
   217  			BaseRepoID: existingPr.BaseRepoID,
   218  			HeadBranch: existingPr.HeadBranch,
   219  			BaseBranch: existingPr.BaseBranch,
   220  		}
   221  	}
   222  	if err != nil && !issues_model.IsErrPullRequestNotExist(err) {
   223  		return err
   224  	}
   225  
   226  	// Set new target branch
   227  	oldBranch := pr.BaseBranch
   228  	pr.BaseBranch = targetBranch
   229  
   230  	// Refresh patch
   231  	if err := TestPatch(pr); err != nil {
   232  		return err
   233  	}
   234  
   235  	// Update target branch, PR diff and status
   236  	// This is the same as checkAndUpdateStatus in check service, but also updates base_branch
   237  	if pr.Status == issues_model.PullRequestStatusChecking {
   238  		pr.Status = issues_model.PullRequestStatusMergeable
   239  	}
   240  
   241  	// Update Commit Divergence
   242  	divergence, err := GetDiverging(ctx, pr)
   243  	if err != nil {
   244  		return err
   245  	}
   246  	pr.CommitsAhead = divergence.Ahead
   247  	pr.CommitsBehind = divergence.Behind
   248  
   249  	if err := pr.UpdateColsIfNotMerged(ctx, "merge_base", "status", "conflicted_files", "changed_protected_files", "base_branch", "commits_ahead", "commits_behind"); err != nil {
   250  		return err
   251  	}
   252  
   253  	// Create comment
   254  	options := &issues_model.CreateCommentOptions{
   255  		Type:   issues_model.CommentTypeChangeTargetBranch,
   256  		Doer:   doer,
   257  		Repo:   pr.Issue.Repo,
   258  		Issue:  pr.Issue,
   259  		OldRef: oldBranch,
   260  		NewRef: targetBranch,
   261  	}
   262  	if _, err = issues_model.CreateComment(ctx, options); err != nil {
   263  		return fmt.Errorf("CreateChangeTargetBranchComment: %w", err)
   264  	}
   265  
   266  	return nil
   267  }
   268  
   269  func checkForInvalidation(ctx context.Context, requests issues_model.PullRequestList, repoID int64, doer *user_model.User, branch string) error {
   270  	repo, err := repo_model.GetRepositoryByID(ctx, repoID)
   271  	if err != nil {
   272  		return fmt.Errorf("GetRepositoryByIDCtx: %w", err)
   273  	}
   274  	gitRepo, err := git.OpenRepository(ctx, repo.RepoPath())
   275  	if err != nil {
   276  		return fmt.Errorf("git.OpenRepository: %w", err)
   277  	}
   278  	go func() {
   279  		// FIXME: graceful: We need to tell the manager we're doing something...
   280  		err := InvalidateCodeComments(ctx, requests, doer, gitRepo, branch)
   281  		if err != nil {
   282  			log.Error("PullRequestList.InvalidateCodeComments: %v", err)
   283  		}
   284  		gitRepo.Close()
   285  	}()
   286  	return nil
   287  }
   288  
   289  // AddTestPullRequestTask adds new test tasks by given head/base repository and head/base branch,
   290  // and generate new patch for testing as needed.
   291  func AddTestPullRequestTask(doer *user_model.User, repoID int64, branch string, isSync bool, oldCommitID, newCommitID string) {
   292  	log.Trace("AddTestPullRequestTask [head_repo_id: %d, head_branch: %s]: finding pull requests", repoID, branch)
   293  	graceful.GetManager().RunWithShutdownContext(func(ctx context.Context) {
   294  		// There is no sensible way to shut this down ":-("
   295  		// If you don't let it run all the way then you will lose data
   296  		// TODO: graceful: AddTestPullRequestTask needs to become a queue!
   297  
   298  		// GetUnmergedPullRequestsByHeadInfo() only return open and unmerged PR.
   299  		prs, err := issues_model.GetUnmergedPullRequestsByHeadInfo(ctx, repoID, branch)
   300  		if err != nil {
   301  			log.Error("Find pull requests [head_repo_id: %d, head_branch: %s]: %v", repoID, branch, err)
   302  			return
   303  		}
   304  
   305  		for _, pr := range prs {
   306  			log.Trace("Updating PR[%d]: composing new test task", pr.ID)
   307  			if pr.Flow == issues_model.PullRequestFlowGithub {
   308  				if err := PushToBaseRepo(ctx, pr); err != nil {
   309  					log.Error("PushToBaseRepo: %v", err)
   310  					continue
   311  				}
   312  			} else {
   313  				continue
   314  			}
   315  
   316  			AddToTaskQueue(ctx, pr)
   317  			comment, err := CreatePushPullComment(ctx, doer, pr, oldCommitID, newCommitID)
   318  			if err == nil && comment != nil {
   319  				notify_service.PullRequestPushCommits(ctx, doer, pr, comment)
   320  			}
   321  		}
   322  
   323  		if isSync {
   324  			requests := issues_model.PullRequestList(prs)
   325  			if err = requests.LoadAttributes(ctx); err != nil {
   326  				log.Error("PullRequestList.LoadAttributes: %v", err)
   327  			}
   328  			if invalidationErr := checkForInvalidation(ctx, requests, repoID, doer, branch); invalidationErr != nil {
   329  				log.Error("checkForInvalidation: %v", invalidationErr)
   330  			}
   331  			if err == nil {
   332  				for _, pr := range prs {
   333  					if newCommitID != "" && newCommitID != git.EmptySHA {
   334  						changed, err := checkIfPRContentChanged(ctx, pr, oldCommitID, newCommitID)
   335  						if err != nil {
   336  							log.Error("checkIfPRContentChanged: %v", err)
   337  						}
   338  						if changed {
   339  							// Mark old reviews as stale if diff to mergebase has changed
   340  							if err := issues_model.MarkReviewsAsStale(ctx, pr.IssueID); err != nil {
   341  								log.Error("MarkReviewsAsStale: %v", err)
   342  							}
   343  
   344  							// dismiss all approval reviews if protected branch rule item enabled.
   345  							pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepoID, pr.BaseBranch)
   346  							if err != nil {
   347  								log.Error("GetFirstMatchProtectedBranchRule: %v", err)
   348  							}
   349  							if pb != nil && pb.DismissStaleApprovals {
   350  								if err := DismissApprovalReviews(ctx, doer, pr); err != nil {
   351  									log.Error("DismissApprovalReviews: %v", err)
   352  								}
   353  							}
   354  						}
   355  						if err := issues_model.MarkReviewsAsNotStale(ctx, pr.IssueID, newCommitID); err != nil {
   356  							log.Error("MarkReviewsAsNotStale: %v", err)
   357  						}
   358  						divergence, err := GetDiverging(ctx, pr)
   359  						if err != nil {
   360  							log.Error("GetDiverging: %v", err)
   361  						} else {
   362  							err = pr.UpdateCommitDivergence(ctx, divergence.Ahead, divergence.Behind)
   363  							if err != nil {
   364  								log.Error("UpdateCommitDivergence: %v", err)
   365  							}
   366  						}
   367  					}
   368  
   369  					notify_service.PullRequestSynchronized(ctx, doer, pr)
   370  				}
   371  			}
   372  		}
   373  
   374  		log.Trace("AddTestPullRequestTask [base_repo_id: %d, base_branch: %s]: finding pull requests", repoID, branch)
   375  		prs, err = issues_model.GetUnmergedPullRequestsByBaseInfo(ctx, repoID, branch)
   376  		if err != nil {
   377  			log.Error("Find pull requests [base_repo_id: %d, base_branch: %s]: %v", repoID, branch, err)
   378  			return
   379  		}
   380  		for _, pr := range prs {
   381  			divergence, err := GetDiverging(ctx, pr)
   382  			if err != nil {
   383  				if git_model.IsErrBranchNotExist(err) && !git.IsBranchExist(ctx, pr.HeadRepo.RepoPath(), pr.HeadBranch) {
   384  					log.Warn("Cannot test PR %s/%d: head_branch %s no longer exists", pr.BaseRepo.Name, pr.IssueID, pr.HeadBranch)
   385  				} else {
   386  					log.Error("GetDiverging: %v", err)
   387  				}
   388  			} else {
   389  				err = pr.UpdateCommitDivergence(ctx, divergence.Ahead, divergence.Behind)
   390  				if err != nil {
   391  					log.Error("UpdateCommitDivergence: %v", err)
   392  				}
   393  			}
   394  			AddToTaskQueue(ctx, pr)
   395  		}
   396  	})
   397  }
   398  
   399  // checkIfPRContentChanged checks if diff to target branch has changed by push
   400  // A commit can be considered to leave the PR untouched if the patch/diff with its merge base is unchanged
   401  func checkIfPRContentChanged(ctx context.Context, pr *issues_model.PullRequest, oldCommitID, newCommitID string) (hasChanged bool, err error) {
   402  	prCtx, cancel, err := createTemporaryRepoForPR(ctx, pr)
   403  	if err != nil {
   404  		log.Error("CreateTemporaryRepoForPR %-v: %v", pr, err)
   405  		return false, err
   406  	}
   407  	defer cancel()
   408  
   409  	tmpRepo, err := git.OpenRepository(ctx, prCtx.tmpBasePath)
   410  	if err != nil {
   411  		return false, fmt.Errorf("OpenRepository: %w", err)
   412  	}
   413  	defer tmpRepo.Close()
   414  
   415  	// Find the merge-base
   416  	_, base, err := tmpRepo.GetMergeBase("", "base", "tracking")
   417  	if err != nil {
   418  		return false, fmt.Errorf("GetMergeBase: %w", err)
   419  	}
   420  
   421  	cmd := git.NewCommand(ctx, "diff", "--name-only", "-z").AddDynamicArguments(newCommitID, oldCommitID, base)
   422  	stdoutReader, stdoutWriter, err := os.Pipe()
   423  	if err != nil {
   424  		return false, fmt.Errorf("unable to open pipe for to run diff: %w", err)
   425  	}
   426  
   427  	stderr := new(bytes.Buffer)
   428  	if err := cmd.Run(&git.RunOpts{
   429  		Dir:    prCtx.tmpBasePath,
   430  		Stdout: stdoutWriter,
   431  		Stderr: stderr,
   432  		PipelineFunc: func(ctx context.Context, cancel context.CancelFunc) error {
   433  			_ = stdoutWriter.Close()
   434  			defer func() {
   435  				_ = stdoutReader.Close()
   436  			}()
   437  			return util.IsEmptyReader(stdoutReader)
   438  		},
   439  	}); err != nil {
   440  		if err == util.ErrNotEmpty {
   441  			return true, nil
   442  		}
   443  		err = git.ConcatenateError(err, stderr.String())
   444  
   445  		log.Error("Unable to run diff on %s %s %s in tempRepo for PR[%d]%s/%s...%s/%s: Error: %v",
   446  			newCommitID, oldCommitID, base,
   447  			pr.ID, pr.BaseRepo.FullName(), pr.BaseBranch, pr.HeadRepo.FullName(), pr.HeadBranch,
   448  			err)
   449  
   450  		return false, fmt.Errorf("Unable to run git diff --name-only -z %s %s %s: %w", newCommitID, oldCommitID, base, err)
   451  	}
   452  
   453  	return false, nil
   454  }
   455  
   456  // PushToBaseRepo pushes commits from branches of head repository to
   457  // corresponding branches of base repository.
   458  // FIXME: Only push branches that are actually updates?
   459  func PushToBaseRepo(ctx context.Context, pr *issues_model.PullRequest) (err error) {
   460  	return pushToBaseRepoHelper(ctx, pr, "")
   461  }
   462  
   463  func pushToBaseRepoHelper(ctx context.Context, pr *issues_model.PullRequest, prefixHeadBranch string) (err error) {
   464  	log.Trace("PushToBaseRepo[%d]: pushing commits to base repo '%s'", pr.BaseRepoID, pr.GetGitRefName())
   465  
   466  	if err := pr.LoadHeadRepo(ctx); err != nil {
   467  		log.Error("Unable to load head repository for PR[%d] Error: %v", pr.ID, err)
   468  		return err
   469  	}
   470  	headRepoPath := pr.HeadRepo.RepoPath()
   471  
   472  	if err := pr.LoadBaseRepo(ctx); err != nil {
   473  		log.Error("Unable to load base repository for PR[%d] Error: %v", pr.ID, err)
   474  		return err
   475  	}
   476  	baseRepoPath := pr.BaseRepo.RepoPath()
   477  
   478  	if err = pr.LoadIssue(ctx); err != nil {
   479  		return fmt.Errorf("unable to load issue %d for pr %d: %w", pr.IssueID, pr.ID, err)
   480  	}
   481  	if err = pr.Issue.LoadPoster(ctx); err != nil {
   482  		return fmt.Errorf("unable to load poster %d for pr %d: %w", pr.Issue.PosterID, pr.ID, err)
   483  	}
   484  
   485  	gitRefName := pr.GetGitRefName()
   486  
   487  	if err := git.Push(ctx, headRepoPath, git.PushOptions{
   488  		Remote: baseRepoPath,
   489  		Branch: prefixHeadBranch + pr.HeadBranch + ":" + gitRefName,
   490  		Force:  true,
   491  		// Use InternalPushingEnvironment here because we know that pre-receive and post-receive do not run on a refs/pulls/...
   492  		Env: repo_module.InternalPushingEnvironment(pr.Issue.Poster, pr.BaseRepo),
   493  	}); err != nil {
   494  		if git.IsErrPushOutOfDate(err) {
   495  			// This should not happen as we're using force!
   496  			log.Error("Unable to push PR head for %s#%d (%-v:%s) due to ErrPushOfDate: %v", pr.BaseRepo.FullName(), pr.Index, pr.BaseRepo, gitRefName, err)
   497  			return err
   498  		} else if git.IsErrPushRejected(err) {
   499  			rejectErr := err.(*git.ErrPushRejected)
   500  			log.Info("Unable to push PR head for %s#%d (%-v:%s) due to rejection:\nStdout: %s\nStderr: %s\nError: %v", pr.BaseRepo.FullName(), pr.Index, pr.BaseRepo, gitRefName, rejectErr.StdOut, rejectErr.StdErr, rejectErr.Err)
   501  			return err
   502  		} else if git.IsErrMoreThanOne(err) {
   503  			if prefixHeadBranch != "" {
   504  				log.Info("Can't push with %s%s", prefixHeadBranch, pr.HeadBranch)
   505  				return err
   506  			}
   507  			log.Info("Retrying to push with %s%s", git.BranchPrefix, pr.HeadBranch)
   508  			err = pushToBaseRepoHelper(ctx, pr, git.BranchPrefix)
   509  			return err
   510  		}
   511  		log.Error("Unable to push PR head for %s#%d (%-v:%s) due to Error: %v", pr.BaseRepo.FullName(), pr.Index, pr.BaseRepo, gitRefName, err)
   512  		return fmt.Errorf("Push: %s:%s %s:%s %w", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), gitRefName, err)
   513  	}
   514  
   515  	return nil
   516  }
   517  
   518  // UpdateRef update refs/pull/id/head directly for agit flow pull request
   519  func UpdateRef(ctx context.Context, pr *issues_model.PullRequest) (err error) {
   520  	log.Trace("UpdateRef[%d]: upgate pull request ref in base repo '%s'", pr.ID, pr.GetGitRefName())
   521  	if err := pr.LoadBaseRepo(ctx); err != nil {
   522  		log.Error("Unable to load base repository for PR[%d] Error: %v", pr.ID, err)
   523  		return err
   524  	}
   525  
   526  	_, _, err = git.NewCommand(ctx, "update-ref").AddDynamicArguments(pr.GetGitRefName(), pr.HeadCommitID).RunStdString(&git.RunOpts{Dir: pr.BaseRepo.RepoPath()})
   527  	if err != nil {
   528  		log.Error("Unable to update ref in base repository for PR[%d] Error: %v", pr.ID, err)
   529  	}
   530  
   531  	return err
   532  }
   533  
   534  type errlist []error
   535  
   536  func (errs errlist) Error() string {
   537  	if len(errs) > 0 {
   538  		var buf strings.Builder
   539  		for i, err := range errs {
   540  			if i > 0 {
   541  				buf.WriteString(", ")
   542  			}
   543  			buf.WriteString(err.Error())
   544  		}
   545  		return buf.String()
   546  	}
   547  	return ""
   548  }
   549  
   550  // CloseBranchPulls close all the pull requests who's head branch is the branch
   551  func CloseBranchPulls(ctx context.Context, doer *user_model.User, repoID int64, branch string) error {
   552  	prs, err := issues_model.GetUnmergedPullRequestsByHeadInfo(ctx, repoID, branch)
   553  	if err != nil {
   554  		return err
   555  	}
   556  
   557  	prs2, err := issues_model.GetUnmergedPullRequestsByBaseInfo(ctx, repoID, branch)
   558  	if err != nil {
   559  		return err
   560  	}
   561  
   562  	prs = append(prs, prs2...)
   563  	if err := issues_model.PullRequestList(prs).LoadAttributes(ctx); err != nil {
   564  		return err
   565  	}
   566  
   567  	var errs errlist
   568  	for _, pr := range prs {
   569  		if err = issue_service.ChangeStatus(ctx, pr.Issue, doer, "", true); err != nil && !issues_model.IsErrPullWasClosed(err) && !issues_model.IsErrDependenciesLeft(err) {
   570  			errs = append(errs, err)
   571  		}
   572  	}
   573  	if len(errs) > 0 {
   574  		return errs
   575  	}
   576  	return nil
   577  }
   578  
   579  // CloseRepoBranchesPulls close all pull requests which head branches are in the given repository, but only whose base repo is not in the given repository
   580  func CloseRepoBranchesPulls(ctx context.Context, doer *user_model.User, repo *repo_model.Repository) error {
   581  	branches, _, err := git.GetBranchesByPath(ctx, repo.RepoPath(), 0, 0)
   582  	if err != nil {
   583  		return err
   584  	}
   585  
   586  	var errs errlist
   587  	for _, branch := range branches {
   588  		prs, err := issues_model.GetUnmergedPullRequestsByHeadInfo(ctx, repo.ID, branch.Name)
   589  		if err != nil {
   590  			return err
   591  		}
   592  
   593  		if err = issues_model.PullRequestList(prs).LoadAttributes(ctx); err != nil {
   594  			return err
   595  		}
   596  
   597  		for _, pr := range prs {
   598  			// If the base repository for this pr is this repository there is no need to close it
   599  			// as it is going to be deleted anyway
   600  			if pr.BaseRepoID == repo.ID {
   601  				continue
   602  			}
   603  			if err = issue_service.ChangeStatus(ctx, pr.Issue, doer, "", true); err != nil && !issues_model.IsErrPullWasClosed(err) {
   604  				errs = append(errs, err)
   605  			}
   606  		}
   607  	}
   608  
   609  	if len(errs) > 0 {
   610  		return errs
   611  	}
   612  	return nil
   613  }
   614  
   615  var commitMessageTrailersPattern = regexp.MustCompile(`(?:^|\n\n)(?:[\w-]+[ \t]*:[^\n]+\n*(?:[ \t]+[^\n]+\n*)*)+$`)
   616  
   617  // GetSquashMergeCommitMessages returns the commit messages between head and merge base (if there is one)
   618  func GetSquashMergeCommitMessages(ctx context.Context, pr *issues_model.PullRequest) string {
   619  	if err := pr.LoadIssue(ctx); err != nil {
   620  		log.Error("Cannot load issue %d for PR id %d: Error: %v", pr.IssueID, pr.ID, err)
   621  		return ""
   622  	}
   623  
   624  	if err := pr.Issue.LoadPoster(ctx); err != nil {
   625  		log.Error("Cannot load poster %d for pr id %d, index %d Error: %v", pr.Issue.PosterID, pr.ID, pr.Index, err)
   626  		return ""
   627  	}
   628  
   629  	if pr.HeadRepo == nil {
   630  		var err error
   631  		pr.HeadRepo, err = repo_model.GetRepositoryByID(ctx, pr.HeadRepoID)
   632  		if err != nil {
   633  			log.Error("GetRepositoryByIdCtx[%d]: %v", pr.HeadRepoID, err)
   634  			return ""
   635  		}
   636  	}
   637  
   638  	gitRepo, closer, err := git.RepositoryFromContextOrOpen(ctx, pr.HeadRepo.RepoPath())
   639  	if err != nil {
   640  		log.Error("Unable to open head repository: Error: %v", err)
   641  		return ""
   642  	}
   643  	defer closer.Close()
   644  
   645  	var headCommit *git.Commit
   646  	if pr.Flow == issues_model.PullRequestFlowGithub {
   647  		headCommit, err = gitRepo.GetBranchCommit(pr.HeadBranch)
   648  	} else {
   649  		pr.HeadCommitID, err = gitRepo.GetRefCommitID(pr.GetGitRefName())
   650  		if err != nil {
   651  			log.Error("Unable to get head commit: %s Error: %v", pr.GetGitRefName(), err)
   652  			return ""
   653  		}
   654  		headCommit, err = gitRepo.GetCommit(pr.HeadCommitID)
   655  	}
   656  	if err != nil {
   657  		log.Error("Unable to get head commit: %s Error: %v", pr.HeadBranch, err)
   658  		return ""
   659  	}
   660  
   661  	mergeBase, err := gitRepo.GetCommit(pr.MergeBase)
   662  	if err != nil {
   663  		log.Error("Unable to get merge base commit: %s Error: %v", pr.MergeBase, err)
   664  		return ""
   665  	}
   666  
   667  	limit := setting.Repository.PullRequest.DefaultMergeMessageCommitsLimit
   668  
   669  	commits, err := gitRepo.CommitsBetweenLimit(headCommit, mergeBase, limit, 0)
   670  	if err != nil {
   671  		log.Error("Unable to get commits between: %s %s Error: %v", pr.HeadBranch, pr.MergeBase, err)
   672  		return ""
   673  	}
   674  
   675  	posterSig := pr.Issue.Poster.NewGitSig().String()
   676  
   677  	uniqueAuthors := make(container.Set[string])
   678  	authors := make([]string, 0, len(commits))
   679  	stringBuilder := strings.Builder{}
   680  
   681  	if !setting.Repository.PullRequest.PopulateSquashCommentWithCommitMessages {
   682  		message := strings.TrimSpace(pr.Issue.Content)
   683  		stringBuilder.WriteString(message)
   684  		if stringBuilder.Len() > 0 {
   685  			stringBuilder.WriteRune('\n')
   686  			if !commitMessageTrailersPattern.MatchString(message) {
   687  				stringBuilder.WriteRune('\n')
   688  			}
   689  		}
   690  	}
   691  
   692  	// commits list is in reverse chronological order
   693  	first := true
   694  	for i := len(commits) - 1; i >= 0; i-- {
   695  		commit := commits[i]
   696  
   697  		if setting.Repository.PullRequest.PopulateSquashCommentWithCommitMessages {
   698  			maxSize := setting.Repository.PullRequest.DefaultMergeMessageSize
   699  			if maxSize < 0 || stringBuilder.Len() < maxSize {
   700  				var toWrite []byte
   701  				if first {
   702  					first = false
   703  					toWrite = []byte(strings.TrimPrefix(commit.CommitMessage, pr.Issue.Title))
   704  				} else {
   705  					toWrite = []byte(commit.CommitMessage)
   706  				}
   707  
   708  				if len(toWrite) > maxSize-stringBuilder.Len() && maxSize > -1 {
   709  					toWrite = append(toWrite[:maxSize-stringBuilder.Len()], "..."...)
   710  				}
   711  				if _, err := stringBuilder.Write(toWrite); err != nil {
   712  					log.Error("Unable to write commit message Error: %v", err)
   713  					return ""
   714  				}
   715  
   716  				if _, err := stringBuilder.WriteRune('\n'); err != nil {
   717  					log.Error("Unable to write commit message Error: %v", err)
   718  					return ""
   719  				}
   720  			}
   721  		}
   722  
   723  		authorString := commit.Author.String()
   724  		if uniqueAuthors.Add(authorString) && authorString != posterSig {
   725  			// Compare use account as well to avoid adding the same author multiple times
   726  			// times when email addresses are private or multiple emails are used.
   727  			commitUser, _ := user_model.GetUserByEmail(ctx, commit.Author.Email)
   728  			if commitUser == nil || commitUser.ID != pr.Issue.Poster.ID {
   729  				authors = append(authors, authorString)
   730  			}
   731  		}
   732  	}
   733  
   734  	// Consider collecting the remaining authors
   735  	if limit >= 0 && setting.Repository.PullRequest.DefaultMergeMessageAllAuthors {
   736  		skip := limit
   737  		limit = 30
   738  		for {
   739  			commits, err := gitRepo.CommitsBetweenLimit(headCommit, mergeBase, limit, skip)
   740  			if err != nil {
   741  				log.Error("Unable to get commits between: %s %s Error: %v", pr.HeadBranch, pr.MergeBase, err)
   742  				return ""
   743  
   744  			}
   745  			if len(commits) == 0 {
   746  				break
   747  			}
   748  			for _, commit := range commits {
   749  				authorString := commit.Author.String()
   750  				if uniqueAuthors.Add(authorString) && authorString != posterSig {
   751  					commitUser, _ := user_model.GetUserByEmail(ctx, commit.Author.Email)
   752  					if commitUser == nil || commitUser.ID != pr.Issue.Poster.ID {
   753  						authors = append(authors, authorString)
   754  					}
   755  				}
   756  			}
   757  			skip += limit
   758  		}
   759  	}
   760  
   761  	for _, author := range authors {
   762  		if _, err := stringBuilder.WriteString("Co-authored-by: "); err != nil {
   763  			log.Error("Unable to write to string builder Error: %v", err)
   764  			return ""
   765  		}
   766  		if _, err := stringBuilder.WriteString(author); err != nil {
   767  			log.Error("Unable to write to string builder Error: %v", err)
   768  			return ""
   769  		}
   770  		if _, err := stringBuilder.WriteRune('\n'); err != nil {
   771  			log.Error("Unable to write to string builder Error: %v", err)
   772  			return ""
   773  		}
   774  	}
   775  
   776  	return stringBuilder.String()
   777  }
   778  
   779  // GetIssuesLastCommitStatus returns a map of issue ID to the most recent commit's latest status
   780  func GetIssuesLastCommitStatus(ctx context.Context, issues issues_model.IssueList) (map[int64]*git_model.CommitStatus, error) {
   781  	_, lastStatus, err := GetIssuesAllCommitStatus(ctx, issues)
   782  	return lastStatus, err
   783  }
   784  
   785  // GetIssuesAllCommitStatus returns a map of issue ID to a list of all statuses for the most recent commit as well as a map of issue ID to only the commit's latest status
   786  func GetIssuesAllCommitStatus(ctx context.Context, issues issues_model.IssueList) (map[int64][]*git_model.CommitStatus, map[int64]*git_model.CommitStatus, error) {
   787  	if err := issues.LoadPullRequests(ctx); err != nil {
   788  		return nil, nil, err
   789  	}
   790  	if _, err := issues.LoadRepositories(ctx); err != nil {
   791  		return nil, nil, err
   792  	}
   793  
   794  	var (
   795  		gitRepos = make(map[int64]*git.Repository)
   796  		res      = make(map[int64][]*git_model.CommitStatus)
   797  		lastRes  = make(map[int64]*git_model.CommitStatus)
   798  		err      error
   799  	)
   800  	defer func() {
   801  		for _, gitRepo := range gitRepos {
   802  			gitRepo.Close()
   803  		}
   804  	}()
   805  
   806  	for _, issue := range issues {
   807  		if !issue.IsPull {
   808  			continue
   809  		}
   810  		gitRepo, ok := gitRepos[issue.RepoID]
   811  		if !ok {
   812  			gitRepo, err = git.OpenRepository(ctx, issue.Repo.RepoPath())
   813  			if err != nil {
   814  				log.Error("Cannot open git repository %-v for issue #%d[%d]. Error: %v", issue.Repo, issue.Index, issue.ID, err)
   815  				continue
   816  			}
   817  			gitRepos[issue.RepoID] = gitRepo
   818  		}
   819  
   820  		statuses, lastStatus, err := getAllCommitStatus(gitRepo, issue.PullRequest)
   821  		if err != nil {
   822  			log.Error("getAllCommitStatus: cant get commit statuses of pull [%d]: %v", issue.PullRequest.ID, err)
   823  			continue
   824  		}
   825  		res[issue.PullRequest.ID] = statuses
   826  		lastRes[issue.PullRequest.ID] = lastStatus
   827  	}
   828  	return res, lastRes, nil
   829  }
   830  
   831  // getAllCommitStatus get pr's commit statuses.
   832  func getAllCommitStatus(gitRepo *git.Repository, pr *issues_model.PullRequest) (statuses []*git_model.CommitStatus, lastStatus *git_model.CommitStatus, err error) {
   833  	sha, shaErr := gitRepo.GetRefCommitID(pr.GetGitRefName())
   834  	if shaErr != nil {
   835  		return nil, nil, shaErr
   836  	}
   837  
   838  	statuses, _, err = git_model.GetLatestCommitStatus(db.DefaultContext, pr.BaseRepo.ID, sha, db.ListOptions{ListAll: true})
   839  	lastStatus = git_model.CalcCommitStatus(statuses)
   840  	return statuses, lastStatus, err
   841  }
   842  
   843  // IsHeadEqualWithBranch returns if the commits of branchName are available in pull request head
   844  func IsHeadEqualWithBranch(ctx context.Context, pr *issues_model.PullRequest, branchName string) (bool, error) {
   845  	var err error
   846  	if err = pr.LoadBaseRepo(ctx); err != nil {
   847  		return false, err
   848  	}
   849  	baseGitRepo, closer, err := git.RepositoryFromContextOrOpen(ctx, pr.BaseRepo.RepoPath())
   850  	if err != nil {
   851  		return false, err
   852  	}
   853  	defer closer.Close()
   854  
   855  	baseCommit, err := baseGitRepo.GetBranchCommit(branchName)
   856  	if err != nil {
   857  		return false, err
   858  	}
   859  
   860  	if err = pr.LoadHeadRepo(ctx); err != nil {
   861  		return false, err
   862  	}
   863  	var headGitRepo *git.Repository
   864  	if pr.HeadRepoID == pr.BaseRepoID {
   865  		headGitRepo = baseGitRepo
   866  	} else {
   867  		var closer io.Closer
   868  
   869  		headGitRepo, closer, err = git.RepositoryFromContextOrOpen(ctx, pr.HeadRepo.RepoPath())
   870  		if err != nil {
   871  			return false, err
   872  		}
   873  		defer closer.Close()
   874  	}
   875  
   876  	var headCommit *git.Commit
   877  	if pr.Flow == issues_model.PullRequestFlowGithub {
   878  		headCommit, err = headGitRepo.GetBranchCommit(pr.HeadBranch)
   879  		if err != nil {
   880  			return false, err
   881  		}
   882  	} else {
   883  		pr.HeadCommitID, err = baseGitRepo.GetRefCommitID(pr.GetGitRefName())
   884  		if err != nil {
   885  			return false, err
   886  		}
   887  		if headCommit, err = baseGitRepo.GetCommit(pr.HeadCommitID); err != nil {
   888  			return false, err
   889  		}
   890  	}
   891  	return baseCommit.HasPreviousCommit(headCommit.ID)
   892  }
   893  
   894  type CommitInfo struct {
   895  	Summary               string `json:"summary"`
   896  	CommitterOrAuthorName string `json:"committer_or_author_name"`
   897  	ID                    string `json:"id"`
   898  	ShortSha              string `json:"short_sha"`
   899  	Time                  string `json:"time"`
   900  }
   901  
   902  // GetPullCommits returns all commits on given pull request and the last review commit sha
   903  func GetPullCommits(ctx *gitea_context.Context, issue *issues_model.Issue) ([]CommitInfo, string, error) {
   904  	pull := issue.PullRequest
   905  
   906  	baseGitRepo := ctx.Repo.GitRepo
   907  
   908  	if err := pull.LoadBaseRepo(ctx); err != nil {
   909  		return nil, "", err
   910  	}
   911  	baseBranch := pull.BaseBranch
   912  	if pull.HasMerged {
   913  		baseBranch = pull.MergeBase
   914  	}
   915  	prInfo, err := baseGitRepo.GetCompareInfo(pull.BaseRepo.RepoPath(), baseBranch, pull.GetGitRefName(), true, false)
   916  	if err != nil {
   917  		return nil, "", err
   918  	}
   919  
   920  	commits := make([]CommitInfo, 0, len(prInfo.Commits))
   921  
   922  	for _, commit := range prInfo.Commits {
   923  		var committerOrAuthorName string
   924  		var commitTime time.Time
   925  		if commit.Committer != nil {
   926  			committerOrAuthorName = commit.Committer.Name
   927  			commitTime = commit.Committer.When
   928  		} else {
   929  			committerOrAuthorName = commit.Author.Name
   930  			commitTime = commit.Author.When
   931  		}
   932  
   933  		commits = append(commits, CommitInfo{
   934  			Summary:               commit.Summary(),
   935  			CommitterOrAuthorName: committerOrAuthorName,
   936  			ID:                    commit.ID.String(),
   937  			ShortSha:              base.ShortSha(commit.ID.String()),
   938  			Time:                  commitTime.Format(time.RFC3339),
   939  		})
   940  	}
   941  
   942  	var lastReviewCommitID string
   943  	if ctx.IsSigned {
   944  		// get last review of current user and store information in context (if available)
   945  		lastreview, err := issues_model.FindLatestReviews(ctx, issues_model.FindReviewOptions{
   946  			IssueID:    issue.ID,
   947  			ReviewerID: ctx.Doer.ID,
   948  			Type:       issues_model.ReviewTypeUnknown,
   949  		})
   950  
   951  		if err != nil && !issues_model.IsErrReviewNotExist(err) {
   952  			return nil, "", err
   953  		}
   954  		if len(lastreview) > 0 {
   955  			lastReviewCommitID = lastreview[0].CommitID
   956  		}
   957  	}
   958  
   959  	return commits, lastReviewCommitID, nil
   960  }