code.gitea.io/gitea@v1.22.3/models/issues/pull.go (about)

     1  // Copyright 2015 The Gogs Authors. All rights reserved.
     2  // Copyright 2019 The Gitea Authors. All rights reserved.
     3  // SPDX-License-Identifier: MIT
     4  
     5  package issues
     6  
     7  import (
     8  	"context"
     9  	"fmt"
    10  	"io"
    11  	"regexp"
    12  	"strconv"
    13  	"strings"
    14  
    15  	"code.gitea.io/gitea/models/db"
    16  	git_model "code.gitea.io/gitea/models/git"
    17  	org_model "code.gitea.io/gitea/models/organization"
    18  	pull_model "code.gitea.io/gitea/models/pull"
    19  	repo_model "code.gitea.io/gitea/models/repo"
    20  	user_model "code.gitea.io/gitea/models/user"
    21  	"code.gitea.io/gitea/modules/git"
    22  	"code.gitea.io/gitea/modules/log"
    23  	"code.gitea.io/gitea/modules/setting"
    24  	"code.gitea.io/gitea/modules/timeutil"
    25  	"code.gitea.io/gitea/modules/util"
    26  
    27  	"xorm.io/builder"
    28  )
    29  
    30  var ErrMustCollaborator = util.NewPermissionDeniedErrorf("user must be a collaborator")
    31  
    32  // ErrPullRequestNotExist represents a "PullRequestNotExist" kind of error.
    33  type ErrPullRequestNotExist struct {
    34  	ID         int64
    35  	IssueID    int64
    36  	HeadRepoID int64
    37  	BaseRepoID int64
    38  	HeadBranch string
    39  	BaseBranch string
    40  }
    41  
    42  // IsErrPullRequestNotExist checks if an error is a ErrPullRequestNotExist.
    43  func IsErrPullRequestNotExist(err error) bool {
    44  	_, ok := err.(ErrPullRequestNotExist)
    45  	return ok
    46  }
    47  
    48  func (err ErrPullRequestNotExist) Error() string {
    49  	return fmt.Sprintf("pull request does not exist [id: %d, issue_id: %d, head_repo_id: %d, base_repo_id: %d, head_branch: %s, base_branch: %s]",
    50  		err.ID, err.IssueID, err.HeadRepoID, err.BaseRepoID, err.HeadBranch, err.BaseBranch)
    51  }
    52  
    53  func (err ErrPullRequestNotExist) Unwrap() error {
    54  	return util.ErrNotExist
    55  }
    56  
    57  // ErrPullRequestAlreadyExists represents a "PullRequestAlreadyExists"-error
    58  type ErrPullRequestAlreadyExists struct {
    59  	ID         int64
    60  	IssueID    int64
    61  	HeadRepoID int64
    62  	BaseRepoID int64
    63  	HeadBranch string
    64  	BaseBranch string
    65  }
    66  
    67  // IsErrPullRequestAlreadyExists checks if an error is a ErrPullRequestAlreadyExists.
    68  func IsErrPullRequestAlreadyExists(err error) bool {
    69  	_, ok := err.(ErrPullRequestAlreadyExists)
    70  	return ok
    71  }
    72  
    73  // Error does pretty-printing :D
    74  func (err ErrPullRequestAlreadyExists) Error() string {
    75  	return fmt.Sprintf("pull request already exists for these targets [id: %d, issue_id: %d, head_repo_id: %d, base_repo_id: %d, head_branch: %s, base_branch: %s]",
    76  		err.ID, err.IssueID, err.HeadRepoID, err.BaseRepoID, err.HeadBranch, err.BaseBranch)
    77  }
    78  
    79  func (err ErrPullRequestAlreadyExists) Unwrap() error {
    80  	return util.ErrAlreadyExist
    81  }
    82  
    83  // ErrPullWasClosed is used close a closed pull request
    84  type ErrPullWasClosed struct {
    85  	ID    int64
    86  	Index int64
    87  }
    88  
    89  // IsErrPullWasClosed checks if an error is a ErrErrPullWasClosed.
    90  func IsErrPullWasClosed(err error) bool {
    91  	_, ok := err.(ErrPullWasClosed)
    92  	return ok
    93  }
    94  
    95  func (err ErrPullWasClosed) Error() string {
    96  	return fmt.Sprintf("Pull request [%d] %d was already closed", err.ID, err.Index)
    97  }
    98  
    99  // PullRequestType defines pull request type
   100  type PullRequestType int
   101  
   102  // Enumerate all the pull request types
   103  const (
   104  	PullRequestGitea PullRequestType = iota
   105  	PullRequestGit
   106  )
   107  
   108  // PullRequestStatus defines pull request status
   109  type PullRequestStatus int
   110  
   111  // Enumerate all the pull request status
   112  const (
   113  	PullRequestStatusConflict PullRequestStatus = iota
   114  	PullRequestStatusChecking
   115  	PullRequestStatusMergeable
   116  	PullRequestStatusManuallyMerged
   117  	PullRequestStatusError
   118  	PullRequestStatusEmpty
   119  	PullRequestStatusAncestor
   120  )
   121  
   122  func (status PullRequestStatus) String() string {
   123  	switch status {
   124  	case PullRequestStatusConflict:
   125  		return "CONFLICT"
   126  	case PullRequestStatusChecking:
   127  		return "CHECKING"
   128  	case PullRequestStatusMergeable:
   129  		return "MERGEABLE"
   130  	case PullRequestStatusManuallyMerged:
   131  		return "MANUALLY_MERGED"
   132  	case PullRequestStatusError:
   133  		return "ERROR"
   134  	case PullRequestStatusEmpty:
   135  		return "EMPTY"
   136  	case PullRequestStatusAncestor:
   137  		return "ANCESTOR"
   138  	default:
   139  		return strconv.Itoa(int(status))
   140  	}
   141  }
   142  
   143  // PullRequestFlow the flow of pull request
   144  type PullRequestFlow int
   145  
   146  const (
   147  	// PullRequestFlowGithub github flow from head branch to base branch
   148  	PullRequestFlowGithub PullRequestFlow = iota
   149  	// PullRequestFlowAGit Agit flow pull request, head branch is not exist
   150  	PullRequestFlowAGit
   151  )
   152  
   153  // PullRequest represents relation between pull request and repositories.
   154  type PullRequest struct {
   155  	ID              int64 `xorm:"pk autoincr"`
   156  	Type            PullRequestType
   157  	Status          PullRequestStatus
   158  	ConflictedFiles []string `xorm:"TEXT JSON"`
   159  	CommitsAhead    int
   160  	CommitsBehind   int
   161  
   162  	ChangedProtectedFiles []string `xorm:"TEXT JSON"`
   163  
   164  	IssueID            int64  `xorm:"INDEX"`
   165  	Issue              *Issue `xorm:"-"`
   166  	Index              int64
   167  	RequestedReviewers []*user_model.User `xorm:"-"`
   168  
   169  	HeadRepoID          int64                  `xorm:"INDEX"`
   170  	HeadRepo            *repo_model.Repository `xorm:"-"`
   171  	BaseRepoID          int64                  `xorm:"INDEX"`
   172  	BaseRepo            *repo_model.Repository `xorm:"-"`
   173  	HeadBranch          string
   174  	HeadCommitID        string `xorm:"-"`
   175  	BaseBranch          string
   176  	MergeBase           string `xorm:"VARCHAR(64)"`
   177  	AllowMaintainerEdit bool   `xorm:"NOT NULL DEFAULT false"`
   178  
   179  	HasMerged      bool               `xorm:"INDEX"`
   180  	MergedCommitID string             `xorm:"VARCHAR(64)"`
   181  	MergerID       int64              `xorm:"INDEX"`
   182  	Merger         *user_model.User   `xorm:"-"`
   183  	MergedUnix     timeutil.TimeStamp `xorm:"updated INDEX"`
   184  
   185  	isHeadRepoLoaded bool `xorm:"-"`
   186  
   187  	Flow PullRequestFlow `xorm:"NOT NULL DEFAULT 0"`
   188  }
   189  
   190  func init() {
   191  	db.RegisterModel(new(PullRequest))
   192  }
   193  
   194  // DeletePullsByBaseRepoID deletes all pull requests by the base repository ID
   195  func DeletePullsByBaseRepoID(ctx context.Context, repoID int64) error {
   196  	deleteCond := builder.Select("id").From("pull_request").Where(builder.Eq{"pull_request.base_repo_id": repoID})
   197  
   198  	// Delete scheduled auto merges
   199  	if _, err := db.GetEngine(ctx).In("pull_id", deleteCond).
   200  		Delete(&pull_model.AutoMerge{}); err != nil {
   201  		return err
   202  	}
   203  
   204  	// Delete review states
   205  	if _, err := db.GetEngine(ctx).In("pull_id", deleteCond).
   206  		Delete(&pull_model.ReviewState{}); err != nil {
   207  		return err
   208  	}
   209  
   210  	_, err := db.DeleteByBean(ctx, &PullRequest{BaseRepoID: repoID})
   211  	return err
   212  }
   213  
   214  func (pr *PullRequest) String() string {
   215  	if pr == nil {
   216  		return "<PullRequest nil>"
   217  	}
   218  
   219  	s := new(strings.Builder)
   220  	fmt.Fprintf(s, "<PullRequest [%d]", pr.ID)
   221  	if pr.BaseRepo != nil {
   222  		fmt.Fprintf(s, "%s#%d[%s...", pr.BaseRepo.FullName(), pr.Index, pr.BaseBranch)
   223  	} else {
   224  		fmt.Fprintf(s, "Repo[%d]#%d[%s...", pr.BaseRepoID, pr.Index, pr.BaseBranch)
   225  	}
   226  	if pr.HeadRepoID == pr.BaseRepoID {
   227  		fmt.Fprintf(s, "%s]", pr.HeadBranch)
   228  	} else if pr.HeadRepo != nil {
   229  		fmt.Fprintf(s, "%s:%s]", pr.HeadRepo.FullName(), pr.HeadBranch)
   230  	} else {
   231  		fmt.Fprintf(s, "Repo[%d]:%s]", pr.HeadRepoID, pr.HeadBranch)
   232  	}
   233  	s.WriteByte('>')
   234  	return s.String()
   235  }
   236  
   237  // MustHeadUserName returns the HeadRepo's username if failed return blank
   238  func (pr *PullRequest) MustHeadUserName(ctx context.Context) string {
   239  	if err := pr.LoadHeadRepo(ctx); err != nil {
   240  		if !repo_model.IsErrRepoNotExist(err) {
   241  			log.Error("LoadHeadRepo: %v", err)
   242  		} else {
   243  			log.Warn("LoadHeadRepo %d but repository does not exist: %v", pr.HeadRepoID, err)
   244  		}
   245  		return ""
   246  	}
   247  	if pr.HeadRepo == nil {
   248  		return ""
   249  	}
   250  	return pr.HeadRepo.OwnerName
   251  }
   252  
   253  // LoadAttributes loads pull request attributes from database
   254  // Note: don't try to get Issue because will end up recursive querying.
   255  func (pr *PullRequest) LoadAttributes(ctx context.Context) (err error) {
   256  	if pr.HasMerged && pr.Merger == nil {
   257  		pr.Merger, err = user_model.GetUserByID(ctx, pr.MergerID)
   258  		if user_model.IsErrUserNotExist(err) {
   259  			pr.MergerID = user_model.GhostUserID
   260  			pr.Merger = user_model.NewGhostUser()
   261  		} else if err != nil {
   262  			return fmt.Errorf("getUserByID [%d]: %w", pr.MergerID, err)
   263  		}
   264  	}
   265  
   266  	return nil
   267  }
   268  
   269  // LoadHeadRepo loads the head repository, pr.HeadRepo will remain nil if it does not exist
   270  // and thus ErrRepoNotExist will never be returned
   271  func (pr *PullRequest) LoadHeadRepo(ctx context.Context) (err error) {
   272  	if !pr.isHeadRepoLoaded && pr.HeadRepo == nil && pr.HeadRepoID > 0 {
   273  		if pr.HeadRepoID == pr.BaseRepoID {
   274  			if pr.BaseRepo != nil {
   275  				pr.HeadRepo = pr.BaseRepo
   276  				return nil
   277  			} else if pr.Issue != nil && pr.Issue.Repo != nil {
   278  				pr.HeadRepo = pr.Issue.Repo
   279  				return nil
   280  			}
   281  		}
   282  
   283  		pr.HeadRepo, err = repo_model.GetRepositoryByID(ctx, pr.HeadRepoID)
   284  		if err != nil && !repo_model.IsErrRepoNotExist(err) { // Head repo maybe deleted, but it should still work
   285  			return fmt.Errorf("pr[%d].LoadHeadRepo[%d]: %w", pr.ID, pr.HeadRepoID, err)
   286  		}
   287  		pr.isHeadRepoLoaded = true
   288  	}
   289  	return nil
   290  }
   291  
   292  // LoadRequestedReviewers loads the requested reviewers.
   293  func (pr *PullRequest) LoadRequestedReviewers(ctx context.Context) error {
   294  	if len(pr.RequestedReviewers) > 0 {
   295  		return nil
   296  	}
   297  
   298  	reviews, err := GetReviewsByIssueID(ctx, pr.Issue.ID)
   299  	if err != nil {
   300  		return err
   301  	}
   302  
   303  	if err = reviews.LoadReviewers(ctx); err != nil {
   304  		return err
   305  	}
   306  	for _, review := range reviews {
   307  		pr.RequestedReviewers = append(pr.RequestedReviewers, review.Reviewer)
   308  	}
   309  
   310  	return nil
   311  }
   312  
   313  // LoadBaseRepo loads the target repository. ErrRepoNotExist may be returned.
   314  func (pr *PullRequest) LoadBaseRepo(ctx context.Context) (err error) {
   315  	if pr.BaseRepo != nil {
   316  		return nil
   317  	}
   318  
   319  	if pr.HeadRepoID == pr.BaseRepoID && pr.HeadRepo != nil {
   320  		pr.BaseRepo = pr.HeadRepo
   321  		return nil
   322  	}
   323  
   324  	if pr.Issue != nil && pr.Issue.Repo != nil {
   325  		pr.BaseRepo = pr.Issue.Repo
   326  		return nil
   327  	}
   328  
   329  	pr.BaseRepo, err = repo_model.GetRepositoryByID(ctx, pr.BaseRepoID)
   330  	if err != nil {
   331  		return fmt.Errorf("pr[%d].LoadBaseRepo[%d]: %w", pr.ID, pr.BaseRepoID, err)
   332  	}
   333  	return nil
   334  }
   335  
   336  // LoadIssue loads issue information from database
   337  func (pr *PullRequest) LoadIssue(ctx context.Context) (err error) {
   338  	if pr.Issue != nil {
   339  		return nil
   340  	}
   341  
   342  	pr.Issue, err = GetIssueByID(ctx, pr.IssueID)
   343  	if err == nil {
   344  		pr.Issue.PullRequest = pr
   345  	}
   346  	return err
   347  }
   348  
   349  // ReviewCount represents a count of Reviews
   350  type ReviewCount struct {
   351  	IssueID int64
   352  	Type    ReviewType
   353  	Count   int64
   354  }
   355  
   356  // GetApprovalCounts returns the approval counts by type
   357  // FIXME: Only returns official counts due to double counting of non-official counts
   358  func (pr *PullRequest) GetApprovalCounts(ctx context.Context) ([]*ReviewCount, error) {
   359  	rCounts := make([]*ReviewCount, 0, 6)
   360  	sess := db.GetEngine(ctx).Where("issue_id = ?", pr.IssueID)
   361  	return rCounts, sess.Select("issue_id, type, count(id) as `count`").Where("official = ? AND dismissed = ?", true, false).GroupBy("issue_id, type").Table("review").Find(&rCounts)
   362  }
   363  
   364  // GetApprovers returns the approvers of the pull request
   365  func (pr *PullRequest) GetApprovers(ctx context.Context) string {
   366  	stringBuilder := strings.Builder{}
   367  	if err := pr.getReviewedByLines(ctx, &stringBuilder); err != nil {
   368  		log.Error("Unable to getReviewedByLines: Error: %v", err)
   369  		return ""
   370  	}
   371  
   372  	return stringBuilder.String()
   373  }
   374  
   375  func (pr *PullRequest) getReviewedByLines(ctx context.Context, writer io.Writer) error {
   376  	maxReviewers := setting.Repository.PullRequest.DefaultMergeMessageMaxApprovers
   377  
   378  	if maxReviewers == 0 {
   379  		return nil
   380  	}
   381  
   382  	ctx, committer, err := db.TxContext(ctx)
   383  	if err != nil {
   384  		return err
   385  	}
   386  	defer committer.Close()
   387  
   388  	// Note: This doesn't page as we only expect a very limited number of reviews
   389  	reviews, err := FindLatestReviews(ctx, FindReviewOptions{
   390  		Types:        []ReviewType{ReviewTypeApprove},
   391  		IssueID:      pr.IssueID,
   392  		OfficialOnly: setting.Repository.PullRequest.DefaultMergeMessageOfficialApproversOnly,
   393  	})
   394  	if err != nil {
   395  		log.Error("Unable to FindReviews for PR ID %d: %v", pr.ID, err)
   396  		return err
   397  	}
   398  
   399  	reviewersWritten := 0
   400  
   401  	for _, review := range reviews {
   402  		if maxReviewers > 0 && reviewersWritten > maxReviewers {
   403  			break
   404  		}
   405  
   406  		if err := review.LoadReviewer(ctx); err != nil && !user_model.IsErrUserNotExist(err) {
   407  			log.Error("Unable to LoadReviewer[%d] for PR ID %d : %v", review.ReviewerID, pr.ID, err)
   408  			return err
   409  		} else if review.Reviewer == nil {
   410  			continue
   411  		}
   412  		if _, err := writer.Write([]byte("Reviewed-by: ")); err != nil {
   413  			return err
   414  		}
   415  		if _, err := writer.Write([]byte(review.Reviewer.NewGitSig().String())); err != nil {
   416  			return err
   417  		}
   418  		if _, err := writer.Write([]byte{'\n'}); err != nil {
   419  			return err
   420  		}
   421  		reviewersWritten++
   422  	}
   423  	return committer.Commit()
   424  }
   425  
   426  // GetGitRefName returns git ref for hidden pull request branch
   427  func (pr *PullRequest) GetGitRefName() string {
   428  	return fmt.Sprintf("%s%d/head", git.PullPrefix, pr.Index)
   429  }
   430  
   431  func (pr *PullRequest) GetGitHeadBranchRefName() string {
   432  	return fmt.Sprintf("%s%s", git.BranchPrefix, pr.HeadBranch)
   433  }
   434  
   435  // GetReviewCommentsCount returns the number of review comments made on the diff of a PR review (not including comments on commits or issues in a PR)
   436  func (pr *PullRequest) GetReviewCommentsCount(ctx context.Context) int {
   437  	opts := FindCommentsOptions{
   438  		Type:    CommentTypeReview,
   439  		IssueID: pr.IssueID,
   440  	}
   441  	conds := opts.ToConds()
   442  
   443  	count, err := db.GetEngine(ctx).Where(conds).Count(new(Comment))
   444  	if err != nil {
   445  		return 0
   446  	}
   447  	return int(count)
   448  }
   449  
   450  // IsChecking returns true if this pull request is still checking conflict.
   451  func (pr *PullRequest) IsChecking() bool {
   452  	return pr.Status == PullRequestStatusChecking
   453  }
   454  
   455  // CanAutoMerge returns true if this pull request can be merged automatically.
   456  func (pr *PullRequest) CanAutoMerge() bool {
   457  	return pr.Status == PullRequestStatusMergeable
   458  }
   459  
   460  // IsEmpty returns true if this pull request is empty.
   461  func (pr *PullRequest) IsEmpty() bool {
   462  	return pr.Status == PullRequestStatusEmpty
   463  }
   464  
   465  // IsAncestor returns true if the Head Commit of this PR is an ancestor of the Base Commit
   466  func (pr *PullRequest) IsAncestor() bool {
   467  	return pr.Status == PullRequestStatusAncestor
   468  }
   469  
   470  // IsFromFork return true if this PR is from a fork.
   471  func (pr *PullRequest) IsFromFork() bool {
   472  	return pr.HeadRepoID != pr.BaseRepoID
   473  }
   474  
   475  // SetMerged sets a pull request to merged and closes the corresponding issue
   476  func (pr *PullRequest) SetMerged(ctx context.Context) (bool, error) {
   477  	if pr.HasMerged {
   478  		return false, fmt.Errorf("PullRequest[%d] already merged", pr.Index)
   479  	}
   480  	if pr.MergedCommitID == "" || pr.MergedUnix == 0 || pr.Merger == nil {
   481  		return false, fmt.Errorf("Unable to merge PullRequest[%d], some required fields are empty", pr.Index)
   482  	}
   483  
   484  	pr.HasMerged = true
   485  	sess := db.GetEngine(ctx)
   486  
   487  	if _, err := sess.Exec("UPDATE `issue` SET `repo_id` = `repo_id` WHERE `id` = ?", pr.IssueID); err != nil {
   488  		return false, err
   489  	}
   490  
   491  	if _, err := sess.Exec("UPDATE `pull_request` SET `issue_id` = `issue_id` WHERE `id` = ?", pr.ID); err != nil {
   492  		return false, err
   493  	}
   494  
   495  	pr.Issue = nil
   496  	if err := pr.LoadIssue(ctx); err != nil {
   497  		return false, err
   498  	}
   499  
   500  	if tmpPr, err := GetPullRequestByID(ctx, pr.ID); err != nil {
   501  		return false, err
   502  	} else if tmpPr.HasMerged {
   503  		if pr.Issue.IsClosed {
   504  			return false, nil
   505  		}
   506  		return false, fmt.Errorf("PullRequest[%d] already merged but it's associated issue [%d] is not closed", pr.Index, pr.IssueID)
   507  	} else if pr.Issue.IsClosed {
   508  		return false, fmt.Errorf("PullRequest[%d] already closed", pr.Index)
   509  	}
   510  
   511  	if err := pr.Issue.LoadRepo(ctx); err != nil {
   512  		return false, err
   513  	}
   514  
   515  	if err := pr.Issue.Repo.LoadOwner(ctx); err != nil {
   516  		return false, err
   517  	}
   518  
   519  	if _, err := changeIssueStatus(ctx, pr.Issue, pr.Merger, true, true); err != nil {
   520  		return false, fmt.Errorf("Issue.changeStatus: %w", err)
   521  	}
   522  
   523  	// reset the conflicted files as there cannot be any if we're merged
   524  	pr.ConflictedFiles = []string{}
   525  
   526  	// We need to save all of the data used to compute this merge as it may have already been changed by TestPatch. FIXME: need to set some state to prevent TestPatch from running whilst we are merging.
   527  	if _, err := sess.Where("id = ?", pr.ID).Cols("has_merged, status, merge_base, merged_commit_id, merger_id, merged_unix, conflicted_files").Update(pr); err != nil {
   528  		return false, fmt.Errorf("Failed to update pr[%d]: %w", pr.ID, err)
   529  	}
   530  
   531  	return true, nil
   532  }
   533  
   534  // NewPullRequest creates new pull request with labels for repository.
   535  func NewPullRequest(ctx context.Context, repo *repo_model.Repository, issue *Issue, labelIDs []int64, uuids []string, pr *PullRequest) (err error) {
   536  	ctx, committer, err := db.TxContext(ctx)
   537  	if err != nil {
   538  		return err
   539  	}
   540  	defer committer.Close()
   541  
   542  	idx, err := db.GetNextResourceIndex(ctx, "issue_index", repo.ID)
   543  	if err != nil {
   544  		return fmt.Errorf("generate pull request index failed: %w", err)
   545  	}
   546  
   547  	issue.Index = idx
   548  
   549  	if err = NewIssueWithIndex(ctx, issue.Poster, NewIssueOptions{
   550  		Repo:        repo,
   551  		Issue:       issue,
   552  		LabelIDs:    labelIDs,
   553  		Attachments: uuids,
   554  		IsPull:      true,
   555  	}); err != nil {
   556  		if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) || IsErrNewIssueInsert(err) {
   557  			return err
   558  		}
   559  		return fmt.Errorf("newIssue: %w", err)
   560  	}
   561  
   562  	pr.Index = issue.Index
   563  	pr.BaseRepo = repo
   564  	pr.IssueID = issue.ID
   565  	if err = db.Insert(ctx, pr); err != nil {
   566  		return fmt.Errorf("insert pull repo: %w", err)
   567  	}
   568  
   569  	if err = committer.Commit(); err != nil {
   570  		return fmt.Errorf("Commit: %w", err)
   571  	}
   572  
   573  	return nil
   574  }
   575  
   576  // ErrUserMustCollaborator represents an error that the user must be a collaborator to a given repo.
   577  type ErrUserMustCollaborator struct {
   578  	UserID   int64
   579  	RepoName string
   580  }
   581  
   582  // GetUnmergedPullRequest returns a pull request that is open and has not been merged
   583  // by given head/base and repo/branch.
   584  func GetUnmergedPullRequest(ctx context.Context, headRepoID, baseRepoID int64, headBranch, baseBranch string, flow PullRequestFlow) (*PullRequest, error) {
   585  	pr := new(PullRequest)
   586  	has, err := db.GetEngine(ctx).
   587  		Where("head_repo_id=? AND head_branch=? AND base_repo_id=? AND base_branch=? AND has_merged=? AND flow = ? AND issue.is_closed=?",
   588  			headRepoID, headBranch, baseRepoID, baseBranch, false, flow, false).
   589  		Join("INNER", "issue", "issue.id=pull_request.issue_id").
   590  		Get(pr)
   591  	if err != nil {
   592  		return nil, err
   593  	} else if !has {
   594  		return nil, ErrPullRequestNotExist{0, 0, headRepoID, baseRepoID, headBranch, baseBranch}
   595  	}
   596  
   597  	return pr, nil
   598  }
   599  
   600  // GetLatestPullRequestByHeadInfo returns the latest pull request (regardless of its status)
   601  // by given head information (repo and branch).
   602  func GetLatestPullRequestByHeadInfo(ctx context.Context, repoID int64, branch string) (*PullRequest, error) {
   603  	pr := new(PullRequest)
   604  	has, err := db.GetEngine(ctx).
   605  		Where("head_repo_id = ? AND head_branch = ? AND flow = ?", repoID, branch, PullRequestFlowGithub).
   606  		OrderBy("id DESC").
   607  		Get(pr)
   608  	if !has {
   609  		return nil, err
   610  	}
   611  	return pr, err
   612  }
   613  
   614  // GetPullRequestByIndex returns a pull request by the given index
   615  func GetPullRequestByIndex(ctx context.Context, repoID, index int64) (*PullRequest, error) {
   616  	if index < 1 {
   617  		return nil, ErrPullRequestNotExist{}
   618  	}
   619  	pr := &PullRequest{
   620  		BaseRepoID: repoID,
   621  		Index:      index,
   622  	}
   623  
   624  	has, err := db.GetEngine(ctx).Get(pr)
   625  	if err != nil {
   626  		return nil, err
   627  	} else if !has {
   628  		return nil, ErrPullRequestNotExist{0, 0, 0, repoID, "", ""}
   629  	}
   630  
   631  	if err = pr.LoadAttributes(ctx); err != nil {
   632  		return nil, err
   633  	}
   634  	if err = pr.LoadIssue(ctx); err != nil {
   635  		return nil, err
   636  	}
   637  
   638  	return pr, nil
   639  }
   640  
   641  // GetPullRequestByID returns a pull request by given ID.
   642  func GetPullRequestByID(ctx context.Context, id int64) (*PullRequest, error) {
   643  	pr := new(PullRequest)
   644  	has, err := db.GetEngine(ctx).ID(id).Get(pr)
   645  	if err != nil {
   646  		return nil, err
   647  	} else if !has {
   648  		return nil, ErrPullRequestNotExist{id, 0, 0, 0, "", ""}
   649  	}
   650  	return pr, pr.LoadAttributes(ctx)
   651  }
   652  
   653  // GetPullRequestByIssueIDWithNoAttributes returns pull request with no attributes loaded by given issue ID.
   654  func GetPullRequestByIssueIDWithNoAttributes(ctx context.Context, issueID int64) (*PullRequest, error) {
   655  	var pr PullRequest
   656  	has, err := db.GetEngine(ctx).Where("issue_id = ?", issueID).Get(&pr)
   657  	if err != nil {
   658  		return nil, err
   659  	}
   660  	if !has {
   661  		return nil, ErrPullRequestNotExist{0, issueID, 0, 0, "", ""}
   662  	}
   663  	return &pr, nil
   664  }
   665  
   666  // GetPullRequestByIssueID returns pull request by given issue ID.
   667  func GetPullRequestByIssueID(ctx context.Context, issueID int64) (*PullRequest, error) {
   668  	pr, exist, err := db.Get[PullRequest](ctx, builder.Eq{"issue_id": issueID})
   669  	if err != nil {
   670  		return nil, err
   671  	} else if !exist {
   672  		return nil, ErrPullRequestNotExist{0, issueID, 0, 0, "", ""}
   673  	}
   674  	return pr, pr.LoadAttributes(ctx)
   675  }
   676  
   677  // GetPullRequestsByBaseHeadInfo returns the pull request by given base and head
   678  func GetPullRequestByBaseHeadInfo(ctx context.Context, baseID, headID int64, base, head string) (*PullRequest, error) {
   679  	pr := &PullRequest{}
   680  	sess := db.GetEngine(ctx).
   681  		Join("INNER", "issue", "issue.id = pull_request.issue_id").
   682  		Where("base_repo_id = ? AND base_branch = ? AND head_repo_id = ? AND head_branch = ?", baseID, base, headID, head)
   683  	has, err := sess.Get(pr)
   684  	if err != nil {
   685  		return nil, err
   686  	}
   687  	if !has {
   688  		return nil, ErrPullRequestNotExist{
   689  			HeadRepoID: headID,
   690  			BaseRepoID: baseID,
   691  			HeadBranch: head,
   692  			BaseBranch: base,
   693  		}
   694  	}
   695  
   696  	if err = pr.LoadAttributes(ctx); err != nil {
   697  		return nil, err
   698  	}
   699  	if err = pr.LoadIssue(ctx); err != nil {
   700  		return nil, err
   701  	}
   702  
   703  	return pr, nil
   704  }
   705  
   706  // GetAllUnmergedAgitPullRequestByPoster get all unmerged agit flow pull request
   707  // By poster id.
   708  func GetAllUnmergedAgitPullRequestByPoster(ctx context.Context, uid int64) ([]*PullRequest, error) {
   709  	pulls := make([]*PullRequest, 0, 10)
   710  
   711  	err := db.GetEngine(ctx).
   712  		Where("has_merged=? AND flow = ? AND issue.is_closed=? AND issue.poster_id=?",
   713  			false, PullRequestFlowAGit, false, uid).
   714  		Join("INNER", "issue", "issue.id=pull_request.issue_id").
   715  		Find(&pulls)
   716  
   717  	return pulls, err
   718  }
   719  
   720  // Update updates all fields of pull request.
   721  func (pr *PullRequest) Update(ctx context.Context) error {
   722  	_, err := db.GetEngine(ctx).ID(pr.ID).AllCols().Update(pr)
   723  	return err
   724  }
   725  
   726  // UpdateCols updates specific fields of pull request.
   727  func (pr *PullRequest) UpdateCols(ctx context.Context, cols ...string) error {
   728  	_, err := db.GetEngine(ctx).ID(pr.ID).Cols(cols...).Update(pr)
   729  	return err
   730  }
   731  
   732  // UpdateColsIfNotMerged updates specific fields of a pull request if it has not been merged
   733  func (pr *PullRequest) UpdateColsIfNotMerged(ctx context.Context, cols ...string) error {
   734  	_, err := db.GetEngine(ctx).Where("id = ? AND has_merged = ?", pr.ID, false).Cols(cols...).Update(pr)
   735  	return err
   736  }
   737  
   738  // IsWorkInProgress determine if the Pull Request is a Work In Progress by its title
   739  // Issue must be set before this method can be called.
   740  func (pr *PullRequest) IsWorkInProgress(ctx context.Context) bool {
   741  	if err := pr.LoadIssue(ctx); err != nil {
   742  		log.Error("LoadIssue: %v", err)
   743  		return false
   744  	}
   745  	return HasWorkInProgressPrefix(pr.Issue.Title)
   746  }
   747  
   748  // HasWorkInProgressPrefix determines if the given PR title has a Work In Progress prefix
   749  func HasWorkInProgressPrefix(title string) bool {
   750  	for _, prefix := range setting.Repository.PullRequest.WorkInProgressPrefixes {
   751  		if strings.HasPrefix(strings.ToUpper(title), strings.ToUpper(prefix)) {
   752  			return true
   753  		}
   754  	}
   755  	return false
   756  }
   757  
   758  // IsFilesConflicted determines if the  Pull Request has changes conflicting with the target branch.
   759  func (pr *PullRequest) IsFilesConflicted() bool {
   760  	return len(pr.ConflictedFiles) > 0
   761  }
   762  
   763  // GetWorkInProgressPrefix returns the prefix used to mark the pull request as a work in progress.
   764  // It returns an empty string when none were found
   765  func (pr *PullRequest) GetWorkInProgressPrefix(ctx context.Context) string {
   766  	if err := pr.LoadIssue(ctx); err != nil {
   767  		log.Error("LoadIssue: %v", err)
   768  		return ""
   769  	}
   770  
   771  	for _, prefix := range setting.Repository.PullRequest.WorkInProgressPrefixes {
   772  		if strings.HasPrefix(strings.ToUpper(pr.Issue.Title), strings.ToUpper(prefix)) {
   773  			return pr.Issue.Title[0:len(prefix)]
   774  		}
   775  	}
   776  	return ""
   777  }
   778  
   779  // UpdateCommitDivergence update Divergence of a pull request
   780  func (pr *PullRequest) UpdateCommitDivergence(ctx context.Context, ahead, behind int) error {
   781  	if pr.ID == 0 {
   782  		return fmt.Errorf("pull ID is 0")
   783  	}
   784  	pr.CommitsAhead = ahead
   785  	pr.CommitsBehind = behind
   786  	_, err := db.GetEngine(ctx).ID(pr.ID).Cols("commits_ahead", "commits_behind").Update(pr)
   787  	return err
   788  }
   789  
   790  // IsSameRepo returns true if base repo and head repo is the same
   791  func (pr *PullRequest) IsSameRepo() bool {
   792  	return pr.BaseRepoID == pr.HeadRepoID
   793  }
   794  
   795  // GetBaseBranchLink returns the relative URL of the base branch
   796  func (pr *PullRequest) GetBaseBranchLink(ctx context.Context) string {
   797  	if err := pr.LoadBaseRepo(ctx); err != nil {
   798  		log.Error("LoadBaseRepo: %v", err)
   799  		return ""
   800  	}
   801  	if pr.BaseRepo == nil {
   802  		return ""
   803  	}
   804  	return pr.BaseRepo.Link() + "/src/branch/" + util.PathEscapeSegments(pr.BaseBranch)
   805  }
   806  
   807  // GetHeadBranchLink returns the relative URL of the head branch
   808  func (pr *PullRequest) GetHeadBranchLink(ctx context.Context) string {
   809  	if pr.Flow == PullRequestFlowAGit {
   810  		return ""
   811  	}
   812  
   813  	if err := pr.LoadHeadRepo(ctx); err != nil {
   814  		log.Error("LoadHeadRepo: %v", err)
   815  		return ""
   816  	}
   817  	if pr.HeadRepo == nil {
   818  		return ""
   819  	}
   820  	return pr.HeadRepo.Link() + "/src/branch/" + util.PathEscapeSegments(pr.HeadBranch)
   821  }
   822  
   823  // UpdateAllowEdits update if PR can be edited from maintainers
   824  func UpdateAllowEdits(ctx context.Context, pr *PullRequest) error {
   825  	if _, err := db.GetEngine(ctx).ID(pr.ID).Cols("allow_maintainer_edit").Update(pr); err != nil {
   826  		return err
   827  	}
   828  	return nil
   829  }
   830  
   831  // Mergeable returns if the pullrequest is mergeable.
   832  func (pr *PullRequest) Mergeable(ctx context.Context) bool {
   833  	// If a pull request isn't mergeable if it's:
   834  	// - Being conflict checked.
   835  	// - Has a conflict.
   836  	// - Received a error while being conflict checked.
   837  	// - Is a work-in-progress pull request.
   838  	return pr.Status != PullRequestStatusChecking && pr.Status != PullRequestStatusConflict &&
   839  		pr.Status != PullRequestStatusError && !pr.IsWorkInProgress(ctx)
   840  }
   841  
   842  // HasEnoughApprovals returns true if pr has enough granted approvals.
   843  func HasEnoughApprovals(ctx context.Context, protectBranch *git_model.ProtectedBranch, pr *PullRequest) bool {
   844  	if protectBranch.RequiredApprovals == 0 {
   845  		return true
   846  	}
   847  	return GetGrantedApprovalsCount(ctx, protectBranch, pr) >= protectBranch.RequiredApprovals
   848  }
   849  
   850  // GetGrantedApprovalsCount returns the number of granted approvals for pr. A granted approval must be authored by a user in an approval whitelist.
   851  func GetGrantedApprovalsCount(ctx context.Context, protectBranch *git_model.ProtectedBranch, pr *PullRequest) int64 {
   852  	sess := db.GetEngine(ctx).Where("issue_id = ?", pr.IssueID).
   853  		And("type = ?", ReviewTypeApprove).
   854  		And("official = ?", true).
   855  		And("dismissed = ?", false)
   856  	if protectBranch.IgnoreStaleApprovals {
   857  		sess = sess.And("stale = ?", false)
   858  	}
   859  	approvals, err := sess.Count(new(Review))
   860  	if err != nil {
   861  		log.Error("GetGrantedApprovalsCount: %v", err)
   862  		return 0
   863  	}
   864  
   865  	return approvals
   866  }
   867  
   868  // MergeBlockedByRejectedReview returns true if merge is blocked by rejected reviews
   869  func MergeBlockedByRejectedReview(ctx context.Context, protectBranch *git_model.ProtectedBranch, pr *PullRequest) bool {
   870  	if !protectBranch.BlockOnRejectedReviews {
   871  		return false
   872  	}
   873  	rejectExist, err := db.GetEngine(ctx).Where("issue_id = ?", pr.IssueID).
   874  		And("type = ?", ReviewTypeReject).
   875  		And("official = ?", true).
   876  		And("dismissed = ?", false).
   877  		Exist(new(Review))
   878  	if err != nil {
   879  		log.Error("MergeBlockedByRejectedReview: %v", err)
   880  		return true
   881  	}
   882  
   883  	return rejectExist
   884  }
   885  
   886  // MergeBlockedByOfficialReviewRequests block merge because of some review request to official reviewer
   887  // of from official review
   888  func MergeBlockedByOfficialReviewRequests(ctx context.Context, protectBranch *git_model.ProtectedBranch, pr *PullRequest) bool {
   889  	if !protectBranch.BlockOnOfficialReviewRequests {
   890  		return false
   891  	}
   892  	has, err := db.GetEngine(ctx).Where("issue_id = ?", pr.IssueID).
   893  		And("type = ?", ReviewTypeRequest).
   894  		And("official = ?", true).
   895  		Exist(new(Review))
   896  	if err != nil {
   897  		log.Error("MergeBlockedByOfficialReviewRequests: %v", err)
   898  		return true
   899  	}
   900  
   901  	return has
   902  }
   903  
   904  // MergeBlockedByOutdatedBranch returns true if merge is blocked by an outdated head branch
   905  func MergeBlockedByOutdatedBranch(protectBranch *git_model.ProtectedBranch, pr *PullRequest) bool {
   906  	return protectBranch.BlockOnOutdatedBranch && pr.CommitsBehind > 0
   907  }
   908  
   909  // GetCodeOwnersFromContent returns the code owners configuration
   910  // Return empty slice if files missing
   911  // Return warning messages on parsing errors
   912  // We're trying to do the best we can when parsing a file.
   913  // Invalid lines are skipped. Non-existent users and teams too.
   914  func GetCodeOwnersFromContent(ctx context.Context, data string) ([]*CodeOwnerRule, []string) {
   915  	if len(data) == 0 {
   916  		return nil, nil
   917  	}
   918  
   919  	rules := make([]*CodeOwnerRule, 0)
   920  	lines := strings.Split(data, "\n")
   921  	warnings := make([]string, 0)
   922  
   923  	for i, line := range lines {
   924  		tokens := TokenizeCodeOwnersLine(line)
   925  		if len(tokens) == 0 {
   926  			continue
   927  		} else if len(tokens) < 2 {
   928  			warnings = append(warnings, fmt.Sprintf("Line: %d: incorrect format", i+1))
   929  			continue
   930  		}
   931  		rule, wr := ParseCodeOwnersLine(ctx, tokens)
   932  		for _, w := range wr {
   933  			warnings = append(warnings, fmt.Sprintf("Line: %d: %s", i+1, w))
   934  		}
   935  		if rule == nil {
   936  			continue
   937  		}
   938  
   939  		rules = append(rules, rule)
   940  	}
   941  
   942  	return rules, warnings
   943  }
   944  
   945  type CodeOwnerRule struct {
   946  	Rule     *regexp.Regexp
   947  	Negative bool
   948  	Users    []*user_model.User
   949  	Teams    []*org_model.Team
   950  }
   951  
   952  func ParseCodeOwnersLine(ctx context.Context, tokens []string) (*CodeOwnerRule, []string) {
   953  	var err error
   954  	rule := &CodeOwnerRule{
   955  		Users:    make([]*user_model.User, 0),
   956  		Teams:    make([]*org_model.Team, 0),
   957  		Negative: strings.HasPrefix(tokens[0], "!"),
   958  	}
   959  
   960  	warnings := make([]string, 0)
   961  
   962  	rule.Rule, err = regexp.Compile(fmt.Sprintf("^%s$", strings.TrimPrefix(tokens[0], "!")))
   963  	if err != nil {
   964  		warnings = append(warnings, fmt.Sprintf("incorrect codeowner regexp: %s", err))
   965  		return nil, warnings
   966  	}
   967  
   968  	for _, user := range tokens[1:] {
   969  		user = strings.TrimPrefix(user, "@")
   970  
   971  		// Only @org/team can contain slashes
   972  		if strings.Contains(user, "/") {
   973  			s := strings.Split(user, "/")
   974  			if len(s) != 2 {
   975  				warnings = append(warnings, fmt.Sprintf("incorrect codeowner group: %s", user))
   976  				continue
   977  			}
   978  			orgName := s[0]
   979  			teamName := s[1]
   980  
   981  			org, err := org_model.GetOrgByName(ctx, orgName)
   982  			if err != nil {
   983  				warnings = append(warnings, fmt.Sprintf("incorrect codeowner organization: %s", user))
   984  				continue
   985  			}
   986  			teams, err := org.LoadTeams(ctx)
   987  			if err != nil {
   988  				warnings = append(warnings, fmt.Sprintf("incorrect codeowner team: %s", user))
   989  				continue
   990  			}
   991  
   992  			for _, team := range teams {
   993  				if team.Name == teamName {
   994  					rule.Teams = append(rule.Teams, team)
   995  				}
   996  			}
   997  		} else {
   998  			u, err := user_model.GetUserByName(ctx, user)
   999  			if err != nil {
  1000  				warnings = append(warnings, fmt.Sprintf("incorrect codeowner user: %s", user))
  1001  				continue
  1002  			}
  1003  			rule.Users = append(rule.Users, u)
  1004  		}
  1005  	}
  1006  
  1007  	if (len(rule.Users) == 0) && (len(rule.Teams) == 0) {
  1008  		warnings = append(warnings, "no users/groups matched")
  1009  		return nil, warnings
  1010  	}
  1011  
  1012  	return rule, warnings
  1013  }
  1014  
  1015  func TokenizeCodeOwnersLine(line string) []string {
  1016  	if len(line) == 0 {
  1017  		return nil
  1018  	}
  1019  
  1020  	line = strings.TrimSpace(line)
  1021  	line = strings.ReplaceAll(line, "\t", " ")
  1022  
  1023  	tokens := make([]string, 0)
  1024  
  1025  	escape := false
  1026  	token := ""
  1027  	for _, char := range line {
  1028  		if escape {
  1029  			token += string(char)
  1030  			escape = false
  1031  		} else if string(char) == "\\" {
  1032  			escape = true
  1033  		} else if string(char) == "#" {
  1034  			break
  1035  		} else if string(char) == " " {
  1036  			if len(token) > 0 {
  1037  				tokens = append(tokens, token)
  1038  				token = ""
  1039  			}
  1040  		} else {
  1041  			token += string(char)
  1042  		}
  1043  	}
  1044  
  1045  	if len(token) > 0 {
  1046  		tokens = append(tokens, token)
  1047  	}
  1048  
  1049  	return tokens
  1050  }
  1051  
  1052  // InsertPullRequests inserted pull requests
  1053  func InsertPullRequests(ctx context.Context, prs ...*PullRequest) error {
  1054  	ctx, committer, err := db.TxContext(ctx)
  1055  	if err != nil {
  1056  		return err
  1057  	}
  1058  	defer committer.Close()
  1059  	sess := db.GetEngine(ctx)
  1060  	for _, pr := range prs {
  1061  		if err := insertIssue(ctx, pr.Issue); err != nil {
  1062  			return err
  1063  		}
  1064  		pr.IssueID = pr.Issue.ID
  1065  		if _, err := sess.NoAutoTime().Insert(pr); err != nil {
  1066  			return err
  1067  		}
  1068  	}
  1069  	return committer.Commit()
  1070  }
  1071  
  1072  // GetPullRequestByMergedCommit returns a merged pull request by the given commit
  1073  func GetPullRequestByMergedCommit(ctx context.Context, repoID int64, sha string) (*PullRequest, error) {
  1074  	pr := new(PullRequest)
  1075  	has, err := db.GetEngine(ctx).Where("base_repo_id = ? AND merged_commit_id = ?", repoID, sha).Get(pr)
  1076  	if err != nil {
  1077  		return nil, err
  1078  	} else if !has {
  1079  		return nil, ErrPullRequestNotExist{0, 0, 0, repoID, "", ""}
  1080  	}
  1081  
  1082  	if err = pr.LoadAttributes(ctx); err != nil {
  1083  		return nil, err
  1084  	}
  1085  	if err = pr.LoadIssue(ctx); err != nil {
  1086  		return nil, err
  1087  	}
  1088  
  1089  	return pr, nil
  1090  }