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 10KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380
  1. // Copyright 2023 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package issues
  4. import (
  5. "context"
  6. "errors"
  7. "fmt"
  8. "code.gitea.io/gitea/models/db"
  9. "code.gitea.io/gitea/modules/util"
  10. "xorm.io/builder"
  11. "xorm.io/xorm"
  12. )
  13. // IssueStats represents issue statistic information.
  14. type IssueStats struct {
  15. OpenCount, ClosedCount int64
  16. YourRepositoriesCount int64
  17. AssignCount int64
  18. CreateCount int64
  19. MentionCount int64
  20. ReviewRequestedCount int64
  21. ReviewedCount int64
  22. }
  23. // Filter modes.
  24. const (
  25. FilterModeAll = iota
  26. FilterModeAssign
  27. FilterModeCreate
  28. FilterModeMention
  29. FilterModeReviewRequested
  30. FilterModeReviewed
  31. FilterModeYourRepositories
  32. )
  33. const (
  34. // MaxQueryParameters represents the max query parameters
  35. // When queries are broken down in parts because of the number
  36. // of parameters, attempt to break by this amount
  37. MaxQueryParameters = 300
  38. )
  39. // CountIssuesByRepo map from repoID to number of issues matching the options
  40. func CountIssuesByRepo(ctx context.Context, opts *IssuesOptions) (map[int64]int64, error) {
  41. sess := db.GetEngine(ctx).
  42. Join("INNER", "repository", "`issue`.repo_id = `repository`.id")
  43. applyConditions(sess, opts)
  44. countsSlice := make([]*struct {
  45. RepoID int64
  46. Count int64
  47. }, 0, 10)
  48. if err := sess.GroupBy("issue.repo_id").
  49. Select("issue.repo_id AS repo_id, COUNT(*) AS count").
  50. Table("issue").
  51. Find(&countsSlice); err != nil {
  52. return nil, fmt.Errorf("unable to CountIssuesByRepo: %w", err)
  53. }
  54. countMap := make(map[int64]int64, len(countsSlice))
  55. for _, c := range countsSlice {
  56. countMap[c.RepoID] = c.Count
  57. }
  58. return countMap, nil
  59. }
  60. // CountIssues number return of issues by given conditions.
  61. func CountIssues(ctx context.Context, opts *IssuesOptions) (int64, error) {
  62. sess := db.GetEngine(ctx).
  63. Select("COUNT(issue.id) AS count").
  64. Table("issue").
  65. Join("INNER", "repository", "`issue`.repo_id = `repository`.id")
  66. applyConditions(sess, opts)
  67. return sess.Count()
  68. }
  69. // GetIssueStats returns issue statistic information by given conditions.
  70. func GetIssueStats(opts *IssuesOptions) (*IssueStats, error) {
  71. if len(opts.IssueIDs) <= MaxQueryParameters {
  72. return getIssueStatsChunk(opts, opts.IssueIDs)
  73. }
  74. // If too long a list of IDs is provided, we get the statistics in
  75. // smaller chunks and get accumulates. Note: this could potentially
  76. // get us invalid results. The alternative is to insert the list of
  77. // ids in a temporary table and join from them.
  78. accum := &IssueStats{}
  79. for i := 0; i < len(opts.IssueIDs); {
  80. chunk := i + MaxQueryParameters
  81. if chunk > len(opts.IssueIDs) {
  82. chunk = len(opts.IssueIDs)
  83. }
  84. stats, err := getIssueStatsChunk(opts, opts.IssueIDs[i:chunk])
  85. if err != nil {
  86. return nil, err
  87. }
  88. accum.OpenCount += stats.OpenCount
  89. accum.ClosedCount += stats.ClosedCount
  90. accum.YourRepositoriesCount += stats.YourRepositoriesCount
  91. accum.AssignCount += stats.AssignCount
  92. accum.CreateCount += stats.CreateCount
  93. accum.OpenCount += stats.MentionCount
  94. accum.ReviewRequestedCount += stats.ReviewRequestedCount
  95. accum.ReviewedCount += stats.ReviewedCount
  96. i = chunk
  97. }
  98. return accum, nil
  99. }
  100. func getIssueStatsChunk(opts *IssuesOptions, issueIDs []int64) (*IssueStats, error) {
  101. stats := &IssueStats{}
  102. countSession := func(opts *IssuesOptions, issueIDs []int64) *xorm.Session {
  103. sess := db.GetEngine(db.DefaultContext).
  104. Join("INNER", "repository", "`issue`.repo_id = `repository`.id")
  105. if len(opts.RepoIDs) > 1 {
  106. sess.In("issue.repo_id", opts.RepoIDs)
  107. } else if len(opts.RepoIDs) == 1 {
  108. sess.And("issue.repo_id = ?", opts.RepoIDs[0])
  109. }
  110. if len(issueIDs) > 0 {
  111. sess.In("issue.id", issueIDs)
  112. }
  113. applyLabelsCondition(sess, opts)
  114. applyMilestoneCondition(sess, opts)
  115. applyProjectCondition(sess, opts)
  116. if opts.AssigneeID > 0 {
  117. applyAssigneeCondition(sess, opts.AssigneeID)
  118. } else if opts.AssigneeID == db.NoConditionID {
  119. sess.Where("issue.id NOT IN (SELECT issue_id FROM issue_assignees)")
  120. }
  121. if opts.PosterID > 0 {
  122. applyPosterCondition(sess, opts.PosterID)
  123. }
  124. if opts.MentionedID > 0 {
  125. applyMentionedCondition(sess, opts.MentionedID)
  126. }
  127. if opts.ReviewRequestedID > 0 {
  128. applyReviewRequestedCondition(sess, opts.ReviewRequestedID)
  129. }
  130. if opts.ReviewedID > 0 {
  131. applyReviewedCondition(sess, opts.ReviewedID)
  132. }
  133. switch opts.IsPull {
  134. case util.OptionalBoolTrue:
  135. sess.And("issue.is_pull=?", true)
  136. case util.OptionalBoolFalse:
  137. sess.And("issue.is_pull=?", false)
  138. }
  139. return sess
  140. }
  141. var err error
  142. stats.OpenCount, err = countSession(opts, issueIDs).
  143. And("issue.is_closed = ?", false).
  144. Count(new(Issue))
  145. if err != nil {
  146. return stats, err
  147. }
  148. stats.ClosedCount, err = countSession(opts, issueIDs).
  149. And("issue.is_closed = ?", true).
  150. Count(new(Issue))
  151. return stats, err
  152. }
  153. // GetUserIssueStats returns issue statistic information for dashboard by given conditions.
  154. func GetUserIssueStats(filterMode int, opts IssuesOptions) (*IssueStats, error) {
  155. if opts.User == nil {
  156. return nil, errors.New("issue stats without user")
  157. }
  158. if opts.IsPull.IsNone() {
  159. return nil, errors.New("unaccepted ispull option")
  160. }
  161. var err error
  162. stats := &IssueStats{}
  163. cond := builder.NewCond()
  164. cond = cond.And(builder.Eq{"issue.is_pull": opts.IsPull.IsTrue()})
  165. if len(opts.RepoIDs) > 0 {
  166. cond = cond.And(builder.In("issue.repo_id", opts.RepoIDs))
  167. }
  168. if len(opts.IssueIDs) > 0 {
  169. cond = cond.And(builder.In("issue.id", opts.IssueIDs))
  170. }
  171. if opts.RepoCond != nil {
  172. cond = cond.And(opts.RepoCond)
  173. }
  174. if opts.User != nil {
  175. cond = cond.And(issuePullAccessibleRepoCond("issue.repo_id", opts.User.ID, opts.Org, opts.Team, opts.IsPull.IsTrue()))
  176. }
  177. sess := func(cond builder.Cond) *xorm.Session {
  178. s := db.GetEngine(db.DefaultContext).
  179. Join("INNER", "repository", "`issue`.repo_id = `repository`.id").
  180. Where(cond)
  181. if len(opts.LabelIDs) > 0 {
  182. s.Join("INNER", "issue_label", "issue_label.issue_id = issue.id").
  183. In("issue_label.label_id", opts.LabelIDs)
  184. }
  185. if opts.IsArchived != util.OptionalBoolNone {
  186. s.And(builder.Eq{"repository.is_archived": opts.IsArchived.IsTrue()})
  187. }
  188. return s
  189. }
  190. switch filterMode {
  191. case FilterModeAll, FilterModeYourRepositories:
  192. stats.OpenCount, err = sess(cond).
  193. And("issue.is_closed = ?", false).
  194. Count(new(Issue))
  195. if err != nil {
  196. return nil, err
  197. }
  198. stats.ClosedCount, err = sess(cond).
  199. And("issue.is_closed = ?", true).
  200. Count(new(Issue))
  201. if err != nil {
  202. return nil, err
  203. }
  204. case FilterModeAssign:
  205. stats.OpenCount, err = applyAssigneeCondition(sess(cond), opts.User.ID).
  206. And("issue.is_closed = ?", false).
  207. Count(new(Issue))
  208. if err != nil {
  209. return nil, err
  210. }
  211. stats.ClosedCount, err = applyAssigneeCondition(sess(cond), opts.User.ID).
  212. And("issue.is_closed = ?", true).
  213. Count(new(Issue))
  214. if err != nil {
  215. return nil, err
  216. }
  217. case FilterModeCreate:
  218. stats.OpenCount, err = applyPosterCondition(sess(cond), opts.User.ID).
  219. And("issue.is_closed = ?", false).
  220. Count(new(Issue))
  221. if err != nil {
  222. return nil, err
  223. }
  224. stats.ClosedCount, err = applyPosterCondition(sess(cond), opts.User.ID).
  225. And("issue.is_closed = ?", true).
  226. Count(new(Issue))
  227. if err != nil {
  228. return nil, err
  229. }
  230. case FilterModeMention:
  231. stats.OpenCount, err = applyMentionedCondition(sess(cond), opts.User.ID).
  232. And("issue.is_closed = ?", false).
  233. Count(new(Issue))
  234. if err != nil {
  235. return nil, err
  236. }
  237. stats.ClosedCount, err = applyMentionedCondition(sess(cond), opts.User.ID).
  238. And("issue.is_closed = ?", true).
  239. Count(new(Issue))
  240. if err != nil {
  241. return nil, err
  242. }
  243. case FilterModeReviewRequested:
  244. stats.OpenCount, err = applyReviewRequestedCondition(sess(cond), opts.User.ID).
  245. And("issue.is_closed = ?", false).
  246. Count(new(Issue))
  247. if err != nil {
  248. return nil, err
  249. }
  250. stats.ClosedCount, err = applyReviewRequestedCondition(sess(cond), opts.User.ID).
  251. And("issue.is_closed = ?", true).
  252. Count(new(Issue))
  253. if err != nil {
  254. return nil, err
  255. }
  256. case FilterModeReviewed:
  257. stats.OpenCount, err = applyReviewedCondition(sess(cond), opts.User.ID).
  258. And("issue.is_closed = ?", false).
  259. Count(new(Issue))
  260. if err != nil {
  261. return nil, err
  262. }
  263. stats.ClosedCount, err = applyReviewedCondition(sess(cond), opts.User.ID).
  264. And("issue.is_closed = ?", true).
  265. Count(new(Issue))
  266. if err != nil {
  267. return nil, err
  268. }
  269. }
  270. cond = cond.And(builder.Eq{"issue.is_closed": opts.IsClosed.IsTrue()})
  271. stats.AssignCount, err = applyAssigneeCondition(sess(cond), opts.User.ID).Count(new(Issue))
  272. if err != nil {
  273. return nil, err
  274. }
  275. stats.CreateCount, err = applyPosterCondition(sess(cond), opts.User.ID).Count(new(Issue))
  276. if err != nil {
  277. return nil, err
  278. }
  279. stats.MentionCount, err = applyMentionedCondition(sess(cond), opts.User.ID).Count(new(Issue))
  280. if err != nil {
  281. return nil, err
  282. }
  283. stats.YourRepositoriesCount, err = sess(cond).Count(new(Issue))
  284. if err != nil {
  285. return nil, err
  286. }
  287. stats.ReviewRequestedCount, err = applyReviewRequestedCondition(sess(cond), opts.User.ID).Count(new(Issue))
  288. if err != nil {
  289. return nil, err
  290. }
  291. stats.ReviewedCount, err = applyReviewedCondition(sess(cond), opts.User.ID).Count(new(Issue))
  292. if err != nil {
  293. return nil, err
  294. }
  295. return stats, nil
  296. }
  297. // GetRepoIssueStats returns number of open and closed repository issues by given filter mode.
  298. func GetRepoIssueStats(repoID, uid int64, filterMode int, isPull bool) (numOpen, numClosed int64) {
  299. countSession := func(isClosed, isPull bool, repoID int64) *xorm.Session {
  300. sess := db.GetEngine(db.DefaultContext).
  301. Where("is_closed = ?", isClosed).
  302. And("is_pull = ?", isPull).
  303. And("repo_id = ?", repoID)
  304. return sess
  305. }
  306. openCountSession := countSession(false, isPull, repoID)
  307. closedCountSession := countSession(true, isPull, repoID)
  308. switch filterMode {
  309. case FilterModeAssign:
  310. applyAssigneeCondition(openCountSession, uid)
  311. applyAssigneeCondition(closedCountSession, uid)
  312. case FilterModeCreate:
  313. applyPosterCondition(openCountSession, uid)
  314. applyPosterCondition(closedCountSession, uid)
  315. }
  316. openResult, _ := openCountSession.Count(new(Issue))
  317. closedResult, _ := closedCountSession.Count(new(Issue))
  318. return openResult, closedResult
  319. }
  320. // CountOrphanedIssues count issues without a repo
  321. func CountOrphanedIssues(ctx context.Context) (int64, error) {
  322. return db.GetEngine(ctx).
  323. Table("issue").
  324. Join("LEFT", "repository", "issue.repo_id=repository.id").
  325. Where(builder.IsNull{"repository.id"}).
  326. Select("COUNT(`issue`.`id`)").
  327. Count()
  328. }