gitea源码

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  1. // Copyright 2023 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. "xorm.io/builder"
  9. "xorm.io/xorm"
  10. )
  11. // IssueStats represents issue statistic information.
  12. type IssueStats struct {
  13. OpenCount, ClosedCount int64
  14. YourRepositoriesCount int64
  15. AssignCount int64
  16. CreateCount int64
  17. MentionCount int64
  18. ReviewRequestedCount int64
  19. ReviewedCount int64
  20. }
  21. // Filter modes.
  22. const (
  23. FilterModeAll = iota
  24. FilterModeAssign
  25. FilterModeCreate
  26. FilterModeMention
  27. FilterModeReviewRequested
  28. FilterModeReviewed
  29. FilterModeYourRepositories
  30. )
  31. const (
  32. // MaxQueryParameters represents the max query parameters
  33. // When queries are broken down in parts because of the number
  34. // of parameters, attempt to break by this amount
  35. MaxQueryParameters = 300
  36. )
  37. // CountIssuesByRepo map from repoID to number of issues matching the options
  38. func CountIssuesByRepo(ctx context.Context, opts *IssuesOptions) (map[int64]int64, error) {
  39. sess := db.GetEngine(ctx).
  40. Join("INNER", "repository", "`issue`.repo_id = `repository`.id")
  41. applyConditions(sess, opts)
  42. countsSlice := make([]*struct {
  43. RepoID int64
  44. Count int64
  45. }, 0, 10)
  46. if err := sess.GroupBy("issue.repo_id").
  47. Select("issue.repo_id AS repo_id, COUNT(*) AS count").
  48. Table("issue").
  49. Find(&countsSlice); err != nil {
  50. return nil, fmt.Errorf("unable to CountIssuesByRepo: %w", err)
  51. }
  52. countMap := make(map[int64]int64, len(countsSlice))
  53. for _, c := range countsSlice {
  54. countMap[c.RepoID] = c.Count
  55. }
  56. return countMap, nil
  57. }
  58. // CountIssues number return of issues by given conditions.
  59. func CountIssues(ctx context.Context, opts *IssuesOptions, otherConds ...builder.Cond) (int64, error) {
  60. sess := db.GetEngine(ctx).
  61. Select("COUNT(issue.id) AS count").
  62. Table("issue").
  63. Join("INNER", "repository", "`issue`.repo_id = `repository`.id")
  64. applyConditions(sess, opts)
  65. for _, cond := range otherConds {
  66. sess.And(cond)
  67. }
  68. return sess.Count()
  69. }
  70. // GetIssueStats returns issue statistic information by given conditions.
  71. func GetIssueStats(ctx context.Context, opts *IssuesOptions) (*IssueStats, error) {
  72. if len(opts.IssueIDs) <= MaxQueryParameters {
  73. return getIssueStatsChunk(ctx, opts, opts.IssueIDs)
  74. }
  75. // If too long a list of IDs is provided, we get the statistics in
  76. // smaller chunks and get accumulates. Note: this could potentially
  77. // get us invalid results. The alternative is to insert the list of
  78. // ids in a temporary table and join from them.
  79. accum := &IssueStats{}
  80. for i := 0; i < len(opts.IssueIDs); {
  81. chunk := min(i+MaxQueryParameters, len(opts.IssueIDs))
  82. stats, err := getIssueStatsChunk(ctx, opts, opts.IssueIDs[i:chunk])
  83. if err != nil {
  84. return nil, err
  85. }
  86. accum.OpenCount += stats.OpenCount
  87. accum.ClosedCount += stats.ClosedCount
  88. accum.YourRepositoriesCount += stats.YourRepositoriesCount
  89. accum.AssignCount += stats.AssignCount
  90. accum.CreateCount += stats.CreateCount
  91. accum.MentionCount += stats.MentionCount
  92. accum.ReviewRequestedCount += stats.ReviewRequestedCount
  93. accum.ReviewedCount += stats.ReviewedCount
  94. i = chunk
  95. }
  96. return accum, nil
  97. }
  98. func getIssueStatsChunk(ctx context.Context, opts *IssuesOptions, issueIDs []int64) (*IssueStats, error) {
  99. stats := &IssueStats{}
  100. sess := db.GetEngine(ctx).
  101. Join("INNER", "repository", "`issue`.repo_id = `repository`.id")
  102. var err error
  103. stats.OpenCount, err = applyIssuesOptions(sess, opts, issueIDs).
  104. And("issue.is_closed = ?", false).
  105. Count(new(Issue))
  106. if err != nil {
  107. return stats, err
  108. }
  109. stats.ClosedCount, err = applyIssuesOptions(sess, opts, issueIDs).
  110. And("issue.is_closed = ?", true).
  111. Count(new(Issue))
  112. return stats, err
  113. }
  114. func applyIssuesOptions(sess *xorm.Session, opts *IssuesOptions, issueIDs []int64) *xorm.Session {
  115. if len(opts.RepoIDs) > 1 {
  116. sess.In("issue.repo_id", opts.RepoIDs)
  117. } else if len(opts.RepoIDs) == 1 {
  118. sess.And("issue.repo_id = ?", opts.RepoIDs[0])
  119. }
  120. if len(issueIDs) > 0 {
  121. sess.In("issue.id", issueIDs)
  122. }
  123. applyLabelsCondition(sess, opts)
  124. applyMilestoneCondition(sess, opts)
  125. applyProjectCondition(sess, opts)
  126. applyAssigneeCondition(sess, opts.AssigneeID)
  127. applyPosterCondition(sess, opts.PosterID)
  128. if opts.MentionedID > 0 {
  129. applyMentionedCondition(sess, opts.MentionedID)
  130. }
  131. if opts.ReviewRequestedID > 0 {
  132. applyReviewRequestedCondition(sess, opts.ReviewRequestedID)
  133. }
  134. if opts.ReviewedID > 0 {
  135. applyReviewedCondition(sess, opts.ReviewedID)
  136. }
  137. if opts.IsPull.Has() {
  138. sess.And("issue.is_pull=?", opts.IsPull.Value())
  139. }
  140. return sess
  141. }
  142. // CountOrphanedIssues count issues without a repo
  143. func CountOrphanedIssues(ctx context.Context) (int64, error) {
  144. return db.GetEngine(ctx).
  145. Table("issue").
  146. Join("LEFT", "repository", "issue.repo_id=repository.id").
  147. Where(builder.IsNull{"repository.id"}).
  148. Select("COUNT(`issue`.`id`)").
  149. Count()
  150. }