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 }