gitea源码

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. // Copyright 2023 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package issues
  4. import (
  5. "context"
  6. "strings"
  7. "code.gitea.io/gitea/models/db"
  8. "code.gitea.io/gitea/modules/optional"
  9. "xorm.io/builder"
  10. )
  11. // MilestoneList is a list of milestones offering additional functionality
  12. type MilestoneList []*Milestone
  13. func (milestones MilestoneList) getMilestoneIDs() []int64 {
  14. ids := make([]int64, 0, len(milestones))
  15. for _, ms := range milestones {
  16. ids = append(ids, ms.ID)
  17. }
  18. return ids
  19. }
  20. // FindMilestoneOptions contain options to get milestones
  21. type FindMilestoneOptions struct {
  22. db.ListOptions
  23. RepoID int64
  24. IsClosed optional.Option[bool]
  25. Name string
  26. SortType string
  27. RepoCond builder.Cond
  28. RepoIDs []int64
  29. }
  30. func (opts FindMilestoneOptions) ToConds() builder.Cond {
  31. cond := builder.NewCond()
  32. if opts.RepoID != 0 {
  33. cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
  34. }
  35. if opts.IsClosed.Has() {
  36. cond = cond.And(builder.Eq{"is_closed": opts.IsClosed.Value()})
  37. }
  38. if opts.RepoCond != nil && opts.RepoCond.IsValid() {
  39. cond = cond.And(builder.In("repo_id", builder.Select("id").From("repository").Where(opts.RepoCond)))
  40. }
  41. if len(opts.RepoIDs) > 0 {
  42. cond = cond.And(builder.In("repo_id", opts.RepoIDs))
  43. }
  44. if len(opts.Name) != 0 {
  45. cond = cond.And(db.BuildCaseInsensitiveLike("name", opts.Name))
  46. }
  47. return cond
  48. }
  49. func (opts FindMilestoneOptions) ToOrders() string {
  50. switch opts.SortType {
  51. case "furthestduedate":
  52. return "deadline_unix DESC"
  53. case "leastcomplete":
  54. return "completeness ASC"
  55. case "mostcomplete":
  56. return "completeness DESC"
  57. case "leastissues":
  58. return "num_issues ASC"
  59. case "mostissues":
  60. return "num_issues DESC"
  61. case "id":
  62. return "id ASC"
  63. case "name":
  64. return "name DESC"
  65. default:
  66. return "deadline_unix ASC, name ASC"
  67. }
  68. }
  69. // GetMilestoneIDsByNames returns a list of milestone ids by given names.
  70. // It doesn't filter them by repo, so it could return milestones belonging to different repos.
  71. // It's used for filtering issues via indexer, otherwise it would be useless.
  72. // Since it could return milestones with the same name, so the length of returned ids could be more than the length of names.
  73. func GetMilestoneIDsByNames(ctx context.Context, names []string) ([]int64, error) {
  74. var ids []int64
  75. return ids, db.GetEngine(ctx).Table("milestone").
  76. Where(db.BuildCaseInsensitiveIn("name", names)).
  77. Cols("id").
  78. Find(&ids)
  79. }
  80. // LoadTotalTrackedTimes loads for every milestone in the list the TotalTrackedTime by a batch request
  81. func (milestones MilestoneList) LoadTotalTrackedTimes(ctx context.Context) error {
  82. type totalTimesByMilestone struct {
  83. MilestoneID int64
  84. Time int64
  85. }
  86. if len(milestones) == 0 {
  87. return nil
  88. }
  89. trackedTimes := make(map[int64]int64, len(milestones))
  90. // Get total tracked time by milestone_id
  91. rows, err := db.GetEngine(ctx).Table("issue").
  92. Join("INNER", "milestone", "issue.milestone_id = milestone.id").
  93. Join("LEFT", "tracked_time", "tracked_time.issue_id = issue.id").
  94. Where("tracked_time.deleted = ?", false).
  95. Select("milestone_id, sum(time) as time").
  96. In("milestone_id", milestones.getMilestoneIDs()).
  97. GroupBy("milestone_id").
  98. Rows(new(totalTimesByMilestone))
  99. if err != nil {
  100. return err
  101. }
  102. defer rows.Close()
  103. for rows.Next() {
  104. var totalTime totalTimesByMilestone
  105. err = rows.Scan(&totalTime)
  106. if err != nil {
  107. return err
  108. }
  109. trackedTimes[totalTime.MilestoneID] = totalTime.Time
  110. }
  111. for _, milestone := range milestones {
  112. milestone.TotalTrackedTime = trackedTimes[milestone.ID]
  113. }
  114. return nil
  115. }
  116. // CountMilestonesByRepoCondAndKw map from repo conditions and the keyword of milestones' name to number of milestones matching the options`
  117. func CountMilestonesMap(ctx context.Context, opts FindMilestoneOptions) (map[int64]int64, error) {
  118. sess := db.GetEngine(ctx).Where(opts.ToConds())
  119. countsSlice := make([]*struct {
  120. RepoID int64
  121. Count int64
  122. }, 0, 10)
  123. if err := sess.GroupBy("repo_id").
  124. Select("repo_id AS repo_id, COUNT(*) AS count").
  125. Table("milestone").
  126. Find(&countsSlice); err != nil {
  127. return nil, err
  128. }
  129. countMap := make(map[int64]int64, len(countsSlice))
  130. for _, c := range countsSlice {
  131. countMap[c.RepoID] = c.Count
  132. }
  133. return countMap, nil
  134. }
  135. // MilestonesStats represents milestone statistic information.
  136. type MilestonesStats struct {
  137. OpenCount, ClosedCount int64
  138. }
  139. // Total returns the total counts of milestones
  140. func (m MilestonesStats) Total() int64 {
  141. return m.OpenCount + m.ClosedCount
  142. }
  143. // GetMilestonesStatsByRepoCondAndKw returns milestone statistic information for dashboard by given repo conditions and name keyword.
  144. func GetMilestonesStatsByRepoCondAndKw(ctx context.Context, repoCond builder.Cond, keyword string) (*MilestonesStats, error) {
  145. var err error
  146. stats := &MilestonesStats{}
  147. sess := db.GetEngine(ctx).Where("is_closed = ?", false)
  148. if len(keyword) > 0 {
  149. sess = sess.And(builder.Like{"UPPER(name)", strings.ToUpper(keyword)})
  150. }
  151. if repoCond.IsValid() {
  152. sess.And(builder.In("repo_id", builder.Select("id").From("repository").Where(repoCond)))
  153. }
  154. stats.OpenCount, err = sess.Count(new(Milestone))
  155. if err != nil {
  156. return nil, err
  157. }
  158. sess = db.GetEngine(ctx).Where("is_closed = ?", true)
  159. if len(keyword) > 0 {
  160. sess = sess.And(builder.Like{"UPPER(name)", strings.ToUpper(keyword)})
  161. }
  162. if repoCond.IsValid() {
  163. sess.And(builder.In("repo_id", builder.Select("id").From("repository").Where(repoCond)))
  164. }
  165. stats.ClosedCount, err = sess.Count(new(Milestone))
  166. if err != nil {
  167. return nil, err
  168. }
  169. return stats, nil
  170. }