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.

issue_stats.go 5.1KB

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