You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

pull_list.go 7.0KB

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