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

     1  // Copyright 2019 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package issues
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  
    10  	"code.gitea.io/gitea/models/db"
    11  	access_model "code.gitea.io/gitea/models/perm/access"
    12  	"code.gitea.io/gitea/models/unit"
    13  	user_model "code.gitea.io/gitea/models/user"
    14  	"code.gitea.io/gitea/modules/log"
    15  	"code.gitea.io/gitea/modules/util"
    16  
    17  	"xorm.io/xorm"
    18  )
    19  
    20  // PullRequestsOptions holds the options for PRs
    21  type PullRequestsOptions struct {
    22  	db.ListOptions
    23  	State       string
    24  	SortType    string
    25  	Labels      []int64
    26  	MilestoneID int64
    27  }
    28  
    29  func listPullRequestStatement(ctx context.Context, baseRepoID int64, opts *PullRequestsOptions) (*xorm.Session, error) {
    30  	sess := db.GetEngine(ctx).Where("pull_request.base_repo_id=?", baseRepoID)
    31  
    32  	sess.Join("INNER", "issue", "pull_request.issue_id = issue.id")
    33  	switch opts.State {
    34  	case "closed", "open":
    35  		sess.And("issue.is_closed=?", opts.State == "closed")
    36  	}
    37  
    38  	if len(opts.Labels) > 0 {
    39  		sess.Join("INNER", "issue_label", "issue.id = issue_label.issue_id").
    40  			In("issue_label.label_id", opts.Labels)
    41  	}
    42  
    43  	if opts.MilestoneID > 0 {
    44  		sess.And("issue.milestone_id=?", opts.MilestoneID)
    45  	}
    46  
    47  	return sess, nil
    48  }
    49  
    50  // GetUnmergedPullRequestsByHeadInfo returns all pull requests that are open and has not been merged
    51  func GetUnmergedPullRequestsByHeadInfo(ctx context.Context, repoID int64, branch string) ([]*PullRequest, error) {
    52  	prs := make([]*PullRequest, 0, 2)
    53  	sess := db.GetEngine(ctx).
    54  		Join("INNER", "issue", "issue.id = pull_request.issue_id").
    55  		Where("head_repo_id = ? AND head_branch = ? AND has_merged = ? AND issue.is_closed = ? AND flow = ?", repoID, branch, false, false, PullRequestFlowGithub)
    56  	return prs, sess.Find(&prs)
    57  }
    58  
    59  // CanMaintainerWriteToBranch check whether user is a maintainer and could write to the branch
    60  func CanMaintainerWriteToBranch(ctx context.Context, p access_model.Permission, branch string, user *user_model.User) bool {
    61  	if p.CanWrite(unit.TypeCode) {
    62  		return true
    63  	}
    64  
    65  	// the code below depends on units to get the repository ID, not ideal but just keep it for now
    66  	firstUnitRepoID := p.GetFirstUnitRepoID()
    67  	if firstUnitRepoID == 0 {
    68  		return false
    69  	}
    70  
    71  	prs, err := GetUnmergedPullRequestsByHeadInfo(ctx, firstUnitRepoID, branch)
    72  	if err != nil {
    73  		return false
    74  	}
    75  
    76  	for _, pr := range prs {
    77  		if pr.AllowMaintainerEdit {
    78  			err = pr.LoadBaseRepo(ctx)
    79  			if err != nil {
    80  				continue
    81  			}
    82  			prPerm, err := access_model.GetUserRepoPermission(ctx, pr.BaseRepo, user)
    83  			if err != nil {
    84  				continue
    85  			}
    86  			if prPerm.CanWrite(unit.TypeCode) {
    87  				return true
    88  			}
    89  		}
    90  	}
    91  	return false
    92  }
    93  
    94  // HasUnmergedPullRequestsByHeadInfo checks if there are open and not merged pull request
    95  // by given head information (repo and branch)
    96  func HasUnmergedPullRequestsByHeadInfo(ctx context.Context, repoID int64, branch string) (bool, error) {
    97  	return db.GetEngine(ctx).
    98  		Where("head_repo_id = ? AND head_branch = ? AND has_merged = ? AND issue.is_closed = ? AND flow = ?",
    99  			repoID, branch, false, false, PullRequestFlowGithub).
   100  		Join("INNER", "issue", "issue.id = pull_request.issue_id").
   101  		Exist(&PullRequest{})
   102  }
   103  
   104  // GetUnmergedPullRequestsByBaseInfo returns all pull requests that are open and has not been merged
   105  // by given base information (repo and branch).
   106  func GetUnmergedPullRequestsByBaseInfo(ctx context.Context, repoID int64, branch string) ([]*PullRequest, error) {
   107  	prs := make([]*PullRequest, 0, 2)
   108  	return prs, db.GetEngine(ctx).
   109  		Where("base_repo_id=? AND base_branch=? AND has_merged=? AND issue.is_closed=?",
   110  			repoID, branch, false, false).
   111  		OrderBy("issue.updated_unix DESC").
   112  		Join("INNER", "issue", "issue.id=pull_request.issue_id").
   113  		Find(&prs)
   114  }
   115  
   116  // GetPullRequestIDsByCheckStatus returns all pull requests according the special checking status.
   117  func GetPullRequestIDsByCheckStatus(ctx context.Context, status PullRequestStatus) ([]int64, error) {
   118  	prs := make([]int64, 0, 10)
   119  	return prs, db.GetEngine(ctx).Table("pull_request").
   120  		Where("status=?", status).
   121  		Cols("pull_request.id").
   122  		Find(&prs)
   123  }
   124  
   125  // PullRequests returns all pull requests for a base Repo by the given conditions
   126  func PullRequests(ctx context.Context, baseRepoID int64, opts *PullRequestsOptions) ([]*PullRequest, int64, error) {
   127  	if opts.Page <= 0 {
   128  		opts.Page = 1
   129  	}
   130  
   131  	countSession, err := listPullRequestStatement(ctx, baseRepoID, opts)
   132  	if err != nil {
   133  		log.Error("listPullRequestStatement: %v", err)
   134  		return nil, 0, err
   135  	}
   136  	maxResults, err := countSession.Count(new(PullRequest))
   137  	if err != nil {
   138  		log.Error("Count PRs: %v", err)
   139  		return nil, maxResults, err
   140  	}
   141  
   142  	findSession, err := listPullRequestStatement(ctx, baseRepoID, opts)
   143  	applySorts(findSession, opts.SortType, 0)
   144  	if err != nil {
   145  		log.Error("listPullRequestStatement: %v", err)
   146  		return nil, maxResults, err
   147  	}
   148  	findSession = db.SetSessionPagination(findSession, opts)
   149  	prs := make([]*PullRequest, 0, opts.PageSize)
   150  	return prs, maxResults, findSession.Find(&prs)
   151  }
   152  
   153  // PullRequestList defines a list of pull requests
   154  type PullRequestList []*PullRequest
   155  
   156  func (prs PullRequestList) LoadAttributes(ctx context.Context) error {
   157  	if len(prs) == 0 {
   158  		return nil
   159  	}
   160  
   161  	// Load issues.
   162  	issueIDs := prs.GetIssueIDs()
   163  	issues := make([]*Issue, 0, len(issueIDs))
   164  	if err := db.GetEngine(ctx).
   165  		Where("id > 0").
   166  		In("id", issueIDs).
   167  		Find(&issues); err != nil {
   168  		return fmt.Errorf("find issues: %w", err)
   169  	}
   170  
   171  	set := make(map[int64]*Issue)
   172  	for i := range issues {
   173  		set[issues[i].ID] = issues[i]
   174  	}
   175  	for _, pr := range prs {
   176  		pr.Issue = set[pr.IssueID]
   177  		/*
   178  			Old code:
   179  			pr.Issue.PullRequest = pr // panic here means issueIDs and prs are not in sync
   180  
   181  			It's worth panic because it's almost impossible to happen under normal use.
   182  			But in integration testing, an asynchronous task could read a database that has been reset.
   183  			So returning an error would make more sense, let the caller has a choice to ignore it.
   184  		*/
   185  		if pr.Issue == nil {
   186  			return fmt.Errorf("issues and prs may be not in sync: cannot find issue %v for pr %v: %w", pr.IssueID, pr.ID, util.ErrNotExist)
   187  		}
   188  		pr.Issue.PullRequest = pr
   189  	}
   190  	return nil
   191  }
   192  
   193  // GetIssueIDs returns all issue ids
   194  func (prs PullRequestList) GetIssueIDs() []int64 {
   195  	issueIDs := make([]int64, 0, len(prs))
   196  	for i := range prs {
   197  		issueIDs = append(issueIDs, prs[i].IssueID)
   198  	}
   199  	return issueIDs
   200  }
   201  
   202  // HasMergedPullRequestInRepo returns whether the user(poster) has merged pull-request in the repo
   203  func HasMergedPullRequestInRepo(ctx context.Context, repoID, posterID int64) (bool, error) {
   204  	return db.GetEngine(ctx).
   205  		Join("INNER", "pull_request", "pull_request.issue_id = issue.id").
   206  		Where("repo_id=?", repoID).
   207  		And("poster_id=?", posterID).
   208  		And("is_pull=?", true).
   209  		And("pull_request.has_merged=?", true).
   210  		Select("issue.id").
   211  		Limit(1).
   212  		Get(new(Issue))
   213  }
   214  
   215  // GetPullRequestByIssueIDs returns all pull requests by issue ids
   216  func GetPullRequestByIssueIDs(ctx context.Context, issueIDs []int64) (PullRequestList, error) {
   217  	prs := make([]*PullRequest, 0, len(issueIDs))
   218  	return prs, db.GetEngine(ctx).
   219  		Where("issue_id > 0").
   220  		In("issue_id", issueIDs).
   221  		Find(&prs)
   222  }