gitea源码

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346
  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. repo_model "code.gitea.io/gitea/models/repo"
  10. "code.gitea.io/gitea/models/unit"
  11. user_model "code.gitea.io/gitea/models/user"
  12. "code.gitea.io/gitea/modules/container"
  13. "code.gitea.io/gitea/modules/log"
  14. "code.gitea.io/gitea/modules/util"
  15. "xorm.io/builder"
  16. "xorm.io/xorm"
  17. )
  18. // PullRequestsOptions holds the options for PRs
  19. type PullRequestsOptions struct {
  20. db.ListOptions
  21. State string
  22. SortType string
  23. Labels []int64
  24. MilestoneID int64
  25. PosterID int64
  26. BaseBranch string
  27. }
  28. func listPullRequestStatement(ctx context.Context, baseRepoID int64, opts *PullRequestsOptions) *xorm.Session {
  29. sess := db.GetEngine(ctx).Where("pull_request.base_repo_id=?", baseRepoID)
  30. if opts.BaseBranch != "" {
  31. sess.And("pull_request.base_branch=?", opts.BaseBranch)
  32. }
  33. sess.Join("INNER", "issue", "pull_request.issue_id = issue.id")
  34. switch opts.State {
  35. case "closed", "open":
  36. sess.And("issue.is_closed=?", opts.State == "closed")
  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. if opts.MilestoneID > 0 {
  43. sess.And("issue.milestone_id=?", opts.MilestoneID)
  44. }
  45. if opts.PosterID > 0 {
  46. sess.And("issue.poster_id=?", opts.PosterID)
  47. }
  48. return sess
  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) (PullRequestList, 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. // CanMaintainerWriteToBranch check whether user is a maintainer and could write to the branch
  59. func CanMaintainerWriteToBranch(ctx context.Context, p access_model.Permission, branch string, user *user_model.User) bool {
  60. if p.CanWrite(unit.TypeCode) {
  61. return true
  62. }
  63. // the code below depends on units to get the repository ID, not ideal but just keep it for now
  64. firstUnitRepoID := p.GetFirstUnitRepoID()
  65. if firstUnitRepoID == 0 {
  66. return false
  67. }
  68. prs, err := GetUnmergedPullRequestsByHeadInfo(ctx, firstUnitRepoID, branch)
  69. if err != nil {
  70. return false
  71. }
  72. for _, pr := range prs {
  73. if pr.AllowMaintainerEdit {
  74. err = pr.LoadBaseRepo(ctx)
  75. if err != nil {
  76. continue
  77. }
  78. prPerm, err := access_model.GetUserRepoPermission(ctx, pr.BaseRepo, user)
  79. if err != nil {
  80. continue
  81. }
  82. if prPerm.CanWrite(unit.TypeCode) {
  83. return true
  84. }
  85. }
  86. }
  87. return false
  88. }
  89. // HasUnmergedPullRequestsByHeadInfo checks if there are open and not merged pull request
  90. // by given head information (repo and branch)
  91. func HasUnmergedPullRequestsByHeadInfo(ctx context.Context, repoID int64, branch string) (bool, error) {
  92. return db.GetEngine(ctx).
  93. Where("head_repo_id = ? AND head_branch = ? AND has_merged = ? AND issue.is_closed = ? AND flow = ?",
  94. repoID, branch, false, false, PullRequestFlowGithub).
  95. Join("INNER", "issue", "issue.id = pull_request.issue_id").
  96. Exist(&PullRequest{})
  97. }
  98. // GetUnmergedPullRequestsByBaseInfo returns all pull requests that are open and has not been merged
  99. // by given base information (repo and branch).
  100. func GetUnmergedPullRequestsByBaseInfo(ctx context.Context, repoID int64, branch string) (PullRequestList, error) {
  101. prs := make([]*PullRequest, 0, 2)
  102. return prs, db.GetEngine(ctx).
  103. Where("base_repo_id=? AND base_branch=? AND has_merged=? AND issue.is_closed=?",
  104. repoID, branch, false, false).
  105. OrderBy("issue.updated_unix DESC").
  106. Join("INNER", "issue", "issue.id=pull_request.issue_id").
  107. Find(&prs)
  108. }
  109. // GetPullRequestIDsByCheckStatus returns all pull requests according the special checking status.
  110. func GetPullRequestIDsByCheckStatus(ctx context.Context, status PullRequestStatus) ([]int64, error) {
  111. prs := make([]int64, 0, 10)
  112. return prs, db.GetEngine(ctx).Table("pull_request").
  113. Where("status=?", status).
  114. Cols("pull_request.id").
  115. Find(&prs)
  116. }
  117. // PullRequests returns all pull requests for a base Repo by the given conditions
  118. func PullRequests(ctx context.Context, baseRepoID int64, opts *PullRequestsOptions) (PullRequestList, int64, error) {
  119. if opts.Page <= 0 {
  120. opts.Page = 1
  121. }
  122. countSession := listPullRequestStatement(ctx, baseRepoID, opts)
  123. maxResults, err := countSession.Count(new(PullRequest))
  124. if err != nil {
  125. log.Error("Count PRs: %v", err)
  126. return nil, maxResults, err
  127. }
  128. findSession := listPullRequestStatement(ctx, baseRepoID, opts)
  129. applySorts(findSession, opts.SortType, 0)
  130. findSession = db.SetSessionPagination(findSession, opts)
  131. prs := make([]*PullRequest, 0, opts.PageSize)
  132. found := findSession.Find(&prs)
  133. return prs, maxResults, found
  134. }
  135. // PullRequestList defines a list of pull requests
  136. type PullRequestList []*PullRequest
  137. func (prs PullRequestList) getRepositoryIDs() []int64 {
  138. repoIDs := make(container.Set[int64])
  139. for _, pr := range prs {
  140. if pr.BaseRepo == nil && pr.BaseRepoID > 0 {
  141. repoIDs.Add(pr.BaseRepoID)
  142. }
  143. if pr.HeadRepo == nil && pr.HeadRepoID > 0 {
  144. repoIDs.Add(pr.HeadRepoID)
  145. }
  146. }
  147. return repoIDs.Values()
  148. }
  149. func (prs PullRequestList) SetBaseRepo(baseRepo *repo_model.Repository) {
  150. for _, pr := range prs {
  151. if pr.BaseRepo == nil {
  152. pr.BaseRepo = baseRepo
  153. }
  154. }
  155. }
  156. func (prs PullRequestList) SetHeadRepo(headRepo *repo_model.Repository) {
  157. for _, pr := range prs {
  158. if pr.HeadRepo == nil {
  159. pr.HeadRepo = headRepo
  160. pr.isHeadRepoLoaded = true
  161. }
  162. }
  163. }
  164. func (prs PullRequestList) LoadRepositories(ctx context.Context) error {
  165. repoIDs := prs.getRepositoryIDs()
  166. reposMap := make(map[int64]*repo_model.Repository, len(repoIDs))
  167. if err := db.GetEngine(ctx).
  168. In("id", repoIDs).
  169. Find(&reposMap); err != nil {
  170. return fmt.Errorf("find repos: %w", err)
  171. }
  172. for _, pr := range prs {
  173. if pr.BaseRepo == nil {
  174. pr.BaseRepo = reposMap[pr.BaseRepoID]
  175. }
  176. if pr.HeadRepo == nil {
  177. pr.HeadRepo = reposMap[pr.HeadRepoID]
  178. pr.isHeadRepoLoaded = true
  179. }
  180. }
  181. return nil
  182. }
  183. func (prs PullRequestList) LoadAttributes(ctx context.Context) error {
  184. if _, err := prs.LoadIssues(ctx); err != nil {
  185. return err
  186. }
  187. return nil
  188. }
  189. func (prs PullRequestList) LoadIssues(ctx context.Context) (IssueList, error) {
  190. if len(prs) == 0 {
  191. return nil, nil
  192. }
  193. // Load issues which are not loaded
  194. issueIDs := container.FilterSlice(prs, func(pr *PullRequest) (int64, bool) {
  195. return pr.IssueID, pr.Issue == nil && pr.IssueID > 0
  196. })
  197. issues := make(map[int64]*Issue, len(issueIDs))
  198. if err := db.GetEngine(ctx).
  199. In("id", issueIDs).
  200. Find(&issues); err != nil {
  201. return nil, fmt.Errorf("find issues: %w", err)
  202. }
  203. issueList := make(IssueList, 0, len(prs))
  204. for _, pr := range prs {
  205. if pr.Issue == nil {
  206. pr.Issue = issues[pr.IssueID]
  207. /*
  208. Old code:
  209. pr.Issue.PullRequest = pr // panic here means issueIDs and prs are not in sync
  210. It's worth panic because it's almost impossible to happen under normal use.
  211. But in integration testing, an asynchronous task could read a database that has been reset.
  212. So returning an error would make more sense, let the caller has a choice to ignore it.
  213. */
  214. if pr.Issue == nil {
  215. return nil, fmt.Errorf("issues and prs may be not in sync: cannot find issue %v for pr %v: %w", pr.IssueID, pr.ID, util.ErrNotExist)
  216. }
  217. }
  218. pr.Issue.PullRequest = pr
  219. if pr.Issue.Repo == nil {
  220. pr.Issue.Repo = pr.BaseRepo
  221. }
  222. issueList = append(issueList, pr.Issue)
  223. }
  224. return issueList, nil
  225. }
  226. // GetIssueIDs returns all issue ids
  227. func (prs PullRequestList) GetIssueIDs() []int64 {
  228. return container.FilterSlice(prs, func(pr *PullRequest) (int64, bool) {
  229. return pr.IssueID, pr.IssueID > 0
  230. })
  231. }
  232. func (prs PullRequestList) LoadReviewCommentsCounts(ctx context.Context) (map[int64]int, error) {
  233. issueIDs := prs.GetIssueIDs()
  234. countsMap := make(map[int64]int, len(issueIDs))
  235. counts := make([]struct {
  236. IssueID int64
  237. Count int
  238. }, 0, len(issueIDs))
  239. if err := db.GetEngine(ctx).Select("issue_id, count(*) as count").
  240. Table("comment").In("issue_id", issueIDs).And("type = ?", CommentTypeReview).
  241. GroupBy("issue_id").Find(&counts); err != nil {
  242. return nil, err
  243. }
  244. for _, c := range counts {
  245. countsMap[c.IssueID] = c.Count
  246. }
  247. return countsMap, nil
  248. }
  249. func (prs PullRequestList) LoadReviews(ctx context.Context) (ReviewList, error) {
  250. issueIDs := prs.GetIssueIDs()
  251. reviews := make([]*Review, 0, len(issueIDs))
  252. subQuery := builder.Select("max(id) as id").
  253. From("review").
  254. Where(builder.In("issue_id", issueIDs)).
  255. And(builder.In("`type`", ReviewTypeApprove, ReviewTypeReject, ReviewTypeRequest)).
  256. And(builder.Eq{
  257. "dismissed": false,
  258. "original_author_id": 0,
  259. "reviewer_team_id": 0,
  260. }).
  261. GroupBy("issue_id, reviewer_id")
  262. // Get latest review of each reviewer, sorted in order they were made
  263. if err := db.GetEngine(ctx).In("id", subQuery).OrderBy("review.updated_unix ASC").Find(&reviews); err != nil {
  264. return nil, err
  265. }
  266. teamReviewRequests := make([]*Review, 0, 5)
  267. subQueryTeam := builder.Select("max(id) as id").
  268. From("review").
  269. Where(builder.In("issue_id", issueIDs)).
  270. And(builder.Eq{
  271. "original_author_id": 0,
  272. }).And(builder.Neq{
  273. "reviewer_team_id": 0,
  274. }).
  275. GroupBy("issue_id, reviewer_team_id")
  276. if err := db.GetEngine(ctx).In("id", subQueryTeam).OrderBy("review.updated_unix ASC").Find(&teamReviewRequests); err != nil {
  277. return nil, err
  278. }
  279. if len(teamReviewRequests) > 0 {
  280. reviews = append(reviews, teamReviewRequests...)
  281. }
  282. return reviews, nil
  283. }
  284. // HasMergedPullRequestInRepo returns whether the user(poster) has merged pull-request in the repo
  285. func HasMergedPullRequestInRepo(ctx context.Context, repoID, posterID int64) (bool, error) {
  286. return db.GetEngine(ctx).
  287. Join("INNER", "pull_request", "pull_request.issue_id = issue.id").
  288. Where("repo_id=?", repoID).
  289. And("poster_id=?", posterID).
  290. And("is_pull=?", true).
  291. And("pull_request.has_merged=?", true).
  292. Select("issue.id").
  293. Limit(1).
  294. Get(new(Issue))
  295. }
  296. // GetPullRequestByIssueIDs returns all pull requests by issue ids
  297. func GetPullRequestByIssueIDs(ctx context.Context, issueIDs []int64) (PullRequestList, error) {
  298. prs := make([]*PullRequest, 0, len(issueIDs))
  299. return prs, db.GetEngine(ctx).
  300. Where("issue_id > 0").
  301. In("issue_id", issueIDs).
  302. Find(&prs)
  303. }