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_list.go 15KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605
  1. // Copyright 2017 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. project_model "code.gitea.io/gitea/models/project"
  9. repo_model "code.gitea.io/gitea/models/repo"
  10. user_model "code.gitea.io/gitea/models/user"
  11. "code.gitea.io/gitea/modules/container"
  12. "xorm.io/builder"
  13. )
  14. // IssueList defines a list of issues
  15. type IssueList []*Issue
  16. // get the repo IDs to be loaded later, these IDs are for issue.Repo and issue.PullRequest.HeadRepo
  17. func (issues IssueList) getRepoIDs() []int64 {
  18. repoIDs := make(container.Set[int64], len(issues))
  19. for _, issue := range issues {
  20. if issue.Repo == nil {
  21. repoIDs.Add(issue.RepoID)
  22. }
  23. if issue.PullRequest != nil && issue.PullRequest.HeadRepo == nil {
  24. repoIDs.Add(issue.PullRequest.HeadRepoID)
  25. }
  26. }
  27. return repoIDs.Values()
  28. }
  29. // LoadRepositories loads issues' all repositories
  30. func (issues IssueList) LoadRepositories(ctx context.Context) ([]*repo_model.Repository, error) {
  31. if len(issues) == 0 {
  32. return nil, nil
  33. }
  34. repoIDs := issues.getRepoIDs()
  35. repoMaps := make(map[int64]*repo_model.Repository, len(repoIDs))
  36. left := len(repoIDs)
  37. for left > 0 {
  38. limit := db.DefaultMaxInSize
  39. if left < limit {
  40. limit = left
  41. }
  42. err := db.GetEngine(ctx).
  43. In("id", repoIDs[:limit]).
  44. Find(&repoMaps)
  45. if err != nil {
  46. return nil, fmt.Errorf("find repository: %w", err)
  47. }
  48. left -= limit
  49. repoIDs = repoIDs[limit:]
  50. }
  51. for _, issue := range issues {
  52. if issue.Repo == nil {
  53. issue.Repo = repoMaps[issue.RepoID]
  54. } else {
  55. repoMaps[issue.RepoID] = issue.Repo
  56. }
  57. if issue.PullRequest != nil {
  58. issue.PullRequest.BaseRepo = issue.Repo
  59. if issue.PullRequest.HeadRepo == nil {
  60. issue.PullRequest.HeadRepo = repoMaps[issue.PullRequest.HeadRepoID]
  61. }
  62. }
  63. }
  64. return repo_model.ValuesRepository(repoMaps), nil
  65. }
  66. func (issues IssueList) getPosterIDs() []int64 {
  67. posterIDs := make(container.Set[int64], len(issues))
  68. for _, issue := range issues {
  69. posterIDs.Add(issue.PosterID)
  70. }
  71. return posterIDs.Values()
  72. }
  73. func (issues IssueList) loadPosters(ctx context.Context) error {
  74. if len(issues) == 0 {
  75. return nil
  76. }
  77. posterMaps, err := getPosters(ctx, issues.getPosterIDs())
  78. if err != nil {
  79. return err
  80. }
  81. for _, issue := range issues {
  82. issue.Poster = getPoster(issue.PosterID, posterMaps)
  83. }
  84. return nil
  85. }
  86. func getPosters(ctx context.Context, posterIDs []int64) (map[int64]*user_model.User, error) {
  87. posterMaps := make(map[int64]*user_model.User, len(posterIDs))
  88. left := len(posterIDs)
  89. for left > 0 {
  90. limit := db.DefaultMaxInSize
  91. if left < limit {
  92. limit = left
  93. }
  94. err := db.GetEngine(ctx).
  95. In("id", posterIDs[:limit]).
  96. Find(&posterMaps)
  97. if err != nil {
  98. return nil, err
  99. }
  100. left -= limit
  101. posterIDs = posterIDs[limit:]
  102. }
  103. return posterMaps, nil
  104. }
  105. func getPoster(posterID int64, posterMaps map[int64]*user_model.User) *user_model.User {
  106. if posterID == user_model.ActionsUserID {
  107. return user_model.NewActionsUser()
  108. }
  109. if posterID <= 0 {
  110. return nil
  111. }
  112. poster, ok := posterMaps[posterID]
  113. if !ok {
  114. return user_model.NewGhostUser()
  115. }
  116. return poster
  117. }
  118. func (issues IssueList) getIssueIDs() []int64 {
  119. ids := make([]int64, 0, len(issues))
  120. for _, issue := range issues {
  121. ids = append(ids, issue.ID)
  122. }
  123. return ids
  124. }
  125. func (issues IssueList) loadLabels(ctx context.Context) error {
  126. if len(issues) == 0 {
  127. return nil
  128. }
  129. type LabelIssue struct {
  130. Label *Label `xorm:"extends"`
  131. IssueLabel *IssueLabel `xorm:"extends"`
  132. }
  133. issueLabels := make(map[int64][]*Label, len(issues)*3)
  134. issueIDs := issues.getIssueIDs()
  135. left := len(issueIDs)
  136. for left > 0 {
  137. limit := db.DefaultMaxInSize
  138. if left < limit {
  139. limit = left
  140. }
  141. rows, err := db.GetEngine(ctx).Table("label").
  142. Join("LEFT", "issue_label", "issue_label.label_id = label.id").
  143. In("issue_label.issue_id", issueIDs[:limit]).
  144. Asc("label.name").
  145. Rows(new(LabelIssue))
  146. if err != nil {
  147. return err
  148. }
  149. for rows.Next() {
  150. var labelIssue LabelIssue
  151. err = rows.Scan(&labelIssue)
  152. if err != nil {
  153. if err1 := rows.Close(); err1 != nil {
  154. return fmt.Errorf("IssueList.loadLabels: Close: %w", err1)
  155. }
  156. return err
  157. }
  158. issueLabels[labelIssue.IssueLabel.IssueID] = append(issueLabels[labelIssue.IssueLabel.IssueID], labelIssue.Label)
  159. }
  160. // When there are no rows left and we try to close it.
  161. // Since that is not relevant for us, we can safely ignore it.
  162. if err1 := rows.Close(); err1 != nil {
  163. return fmt.Errorf("IssueList.loadLabels: Close: %w", err1)
  164. }
  165. left -= limit
  166. issueIDs = issueIDs[limit:]
  167. }
  168. for _, issue := range issues {
  169. issue.Labels = issueLabels[issue.ID]
  170. }
  171. return nil
  172. }
  173. func (issues IssueList) getMilestoneIDs() []int64 {
  174. ids := make(container.Set[int64], len(issues))
  175. for _, issue := range issues {
  176. ids.Add(issue.MilestoneID)
  177. }
  178. return ids.Values()
  179. }
  180. func (issues IssueList) loadMilestones(ctx context.Context) error {
  181. milestoneIDs := issues.getMilestoneIDs()
  182. if len(milestoneIDs) == 0 {
  183. return nil
  184. }
  185. milestoneMaps := make(map[int64]*Milestone, len(milestoneIDs))
  186. left := len(milestoneIDs)
  187. for left > 0 {
  188. limit := db.DefaultMaxInSize
  189. if left < limit {
  190. limit = left
  191. }
  192. err := db.GetEngine(ctx).
  193. In("id", milestoneIDs[:limit]).
  194. Find(&milestoneMaps)
  195. if err != nil {
  196. return err
  197. }
  198. left -= limit
  199. milestoneIDs = milestoneIDs[limit:]
  200. }
  201. for _, issue := range issues {
  202. issue.Milestone = milestoneMaps[issue.MilestoneID]
  203. }
  204. return nil
  205. }
  206. func (issues IssueList) getProjectIDs() []int64 {
  207. ids := make(container.Set[int64], len(issues))
  208. for _, issue := range issues {
  209. ids.Add(issue.ProjectID())
  210. }
  211. return ids.Values()
  212. }
  213. func (issues IssueList) loadProjects(ctx context.Context) error {
  214. projectIDs := issues.getProjectIDs()
  215. if len(projectIDs) == 0 {
  216. return nil
  217. }
  218. projectMaps := make(map[int64]*project_model.Project, len(projectIDs))
  219. left := len(projectIDs)
  220. for left > 0 {
  221. limit := db.DefaultMaxInSize
  222. if left < limit {
  223. limit = left
  224. }
  225. err := db.GetEngine(ctx).
  226. In("id", projectIDs[:limit]).
  227. Find(&projectMaps)
  228. if err != nil {
  229. return err
  230. }
  231. left -= limit
  232. projectIDs = projectIDs[limit:]
  233. }
  234. for _, issue := range issues {
  235. issue.Project = projectMaps[issue.ProjectID()]
  236. }
  237. return nil
  238. }
  239. func (issues IssueList) loadAssignees(ctx context.Context) error {
  240. if len(issues) == 0 {
  241. return nil
  242. }
  243. type AssigneeIssue struct {
  244. IssueAssignee *IssueAssignees `xorm:"extends"`
  245. Assignee *user_model.User `xorm:"extends"`
  246. }
  247. assignees := make(map[int64][]*user_model.User, len(issues))
  248. issueIDs := issues.getIssueIDs()
  249. left := len(issueIDs)
  250. for left > 0 {
  251. limit := db.DefaultMaxInSize
  252. if left < limit {
  253. limit = left
  254. }
  255. rows, err := db.GetEngine(ctx).Table("issue_assignees").
  256. Join("INNER", "`user`", "`user`.id = `issue_assignees`.assignee_id").
  257. In("`issue_assignees`.issue_id", issueIDs[:limit]).OrderBy(user_model.GetOrderByName()).
  258. Rows(new(AssigneeIssue))
  259. if err != nil {
  260. return err
  261. }
  262. for rows.Next() {
  263. var assigneeIssue AssigneeIssue
  264. err = rows.Scan(&assigneeIssue)
  265. if err != nil {
  266. if err1 := rows.Close(); err1 != nil {
  267. return fmt.Errorf("IssueList.loadAssignees: Close: %w", err1)
  268. }
  269. return err
  270. }
  271. assignees[assigneeIssue.IssueAssignee.IssueID] = append(assignees[assigneeIssue.IssueAssignee.IssueID], assigneeIssue.Assignee)
  272. }
  273. if err1 := rows.Close(); err1 != nil {
  274. return fmt.Errorf("IssueList.loadAssignees: Close: %w", err1)
  275. }
  276. left -= limit
  277. issueIDs = issueIDs[limit:]
  278. }
  279. for _, issue := range issues {
  280. issue.Assignees = assignees[issue.ID]
  281. }
  282. return nil
  283. }
  284. func (issues IssueList) getPullIssueIDs() []int64 {
  285. ids := make([]int64, 0, len(issues))
  286. for _, issue := range issues {
  287. if issue.IsPull && issue.PullRequest == nil {
  288. ids = append(ids, issue.ID)
  289. }
  290. }
  291. return ids
  292. }
  293. // LoadPullRequests loads pull requests
  294. func (issues IssueList) LoadPullRequests(ctx context.Context) error {
  295. issuesIDs := issues.getPullIssueIDs()
  296. if len(issuesIDs) == 0 {
  297. return nil
  298. }
  299. pullRequestMaps := make(map[int64]*PullRequest, len(issuesIDs))
  300. left := len(issuesIDs)
  301. for left > 0 {
  302. limit := db.DefaultMaxInSize
  303. if left < limit {
  304. limit = left
  305. }
  306. rows, err := db.GetEngine(ctx).
  307. In("issue_id", issuesIDs[:limit]).
  308. Rows(new(PullRequest))
  309. if err != nil {
  310. return err
  311. }
  312. for rows.Next() {
  313. var pr PullRequest
  314. err = rows.Scan(&pr)
  315. if err != nil {
  316. if err1 := rows.Close(); err1 != nil {
  317. return fmt.Errorf("IssueList.loadPullRequests: Close: %w", err1)
  318. }
  319. return err
  320. }
  321. pullRequestMaps[pr.IssueID] = &pr
  322. }
  323. if err1 := rows.Close(); err1 != nil {
  324. return fmt.Errorf("IssueList.loadPullRequests: Close: %w", err1)
  325. }
  326. left -= limit
  327. issuesIDs = issuesIDs[limit:]
  328. }
  329. for _, issue := range issues {
  330. issue.PullRequest = pullRequestMaps[issue.ID]
  331. }
  332. return nil
  333. }
  334. // LoadAttachments loads attachments
  335. func (issues IssueList) LoadAttachments(ctx context.Context) (err error) {
  336. if len(issues) == 0 {
  337. return nil
  338. }
  339. attachments := make(map[int64][]*repo_model.Attachment, len(issues))
  340. issuesIDs := issues.getIssueIDs()
  341. left := len(issuesIDs)
  342. for left > 0 {
  343. limit := db.DefaultMaxInSize
  344. if left < limit {
  345. limit = left
  346. }
  347. rows, err := db.GetEngine(ctx).Table("attachment").
  348. Join("INNER", "issue", "issue.id = attachment.issue_id").
  349. In("issue.id", issuesIDs[:limit]).
  350. Rows(new(repo_model.Attachment))
  351. if err != nil {
  352. return err
  353. }
  354. for rows.Next() {
  355. var attachment repo_model.Attachment
  356. err = rows.Scan(&attachment)
  357. if err != nil {
  358. if err1 := rows.Close(); err1 != nil {
  359. return fmt.Errorf("IssueList.loadAttachments: Close: %w", err1)
  360. }
  361. return err
  362. }
  363. attachments[attachment.IssueID] = append(attachments[attachment.IssueID], &attachment)
  364. }
  365. if err1 := rows.Close(); err1 != nil {
  366. return fmt.Errorf("IssueList.loadAttachments: Close: %w", err1)
  367. }
  368. left -= limit
  369. issuesIDs = issuesIDs[limit:]
  370. }
  371. for _, issue := range issues {
  372. issue.Attachments = attachments[issue.ID]
  373. }
  374. return nil
  375. }
  376. func (issues IssueList) loadComments(ctx context.Context, cond builder.Cond) (err error) {
  377. if len(issues) == 0 {
  378. return nil
  379. }
  380. comments := make(map[int64][]*Comment, len(issues))
  381. issuesIDs := issues.getIssueIDs()
  382. left := len(issuesIDs)
  383. for left > 0 {
  384. limit := db.DefaultMaxInSize
  385. if left < limit {
  386. limit = left
  387. }
  388. rows, err := db.GetEngine(ctx).Table("comment").
  389. Join("INNER", "issue", "issue.id = comment.issue_id").
  390. In("issue.id", issuesIDs[:limit]).
  391. Where(cond).
  392. Rows(new(Comment))
  393. if err != nil {
  394. return err
  395. }
  396. for rows.Next() {
  397. var comment Comment
  398. err = rows.Scan(&comment)
  399. if err != nil {
  400. if err1 := rows.Close(); err1 != nil {
  401. return fmt.Errorf("IssueList.loadComments: Close: %w", err1)
  402. }
  403. return err
  404. }
  405. comments[comment.IssueID] = append(comments[comment.IssueID], &comment)
  406. }
  407. if err1 := rows.Close(); err1 != nil {
  408. return fmt.Errorf("IssueList.loadComments: Close: %w", err1)
  409. }
  410. left -= limit
  411. issuesIDs = issuesIDs[limit:]
  412. }
  413. for _, issue := range issues {
  414. issue.Comments = comments[issue.ID]
  415. }
  416. return nil
  417. }
  418. func (issues IssueList) loadTotalTrackedTimes(ctx context.Context) (err error) {
  419. type totalTimesByIssue struct {
  420. IssueID int64
  421. Time int64
  422. }
  423. if len(issues) == 0 {
  424. return nil
  425. }
  426. trackedTimes := make(map[int64]int64, len(issues))
  427. ids := make([]int64, 0, len(issues))
  428. for _, issue := range issues {
  429. if issue.Repo.IsTimetrackerEnabled(ctx) {
  430. ids = append(ids, issue.ID)
  431. }
  432. }
  433. left := len(ids)
  434. for left > 0 {
  435. limit := db.DefaultMaxInSize
  436. if left < limit {
  437. limit = left
  438. }
  439. // select issue_id, sum(time) from tracked_time where issue_id in (<issue ids in current page>) group by issue_id
  440. rows, err := db.GetEngine(ctx).Table("tracked_time").
  441. Where("deleted = ?", false).
  442. Select("issue_id, sum(time) as time").
  443. In("issue_id", ids[:limit]).
  444. GroupBy("issue_id").
  445. Rows(new(totalTimesByIssue))
  446. if err != nil {
  447. return err
  448. }
  449. for rows.Next() {
  450. var totalTime totalTimesByIssue
  451. err = rows.Scan(&totalTime)
  452. if err != nil {
  453. if err1 := rows.Close(); err1 != nil {
  454. return fmt.Errorf("IssueList.loadTotalTrackedTimes: Close: %w", err1)
  455. }
  456. return err
  457. }
  458. trackedTimes[totalTime.IssueID] = totalTime.Time
  459. }
  460. if err1 := rows.Close(); err1 != nil {
  461. return fmt.Errorf("IssueList.loadTotalTrackedTimes: Close: %w", err1)
  462. }
  463. left -= limit
  464. ids = ids[limit:]
  465. }
  466. for _, issue := range issues {
  467. issue.TotalTrackedTime = trackedTimes[issue.ID]
  468. }
  469. return nil
  470. }
  471. // loadAttributes loads all attributes, expect for attachments and comments
  472. func (issues IssueList) loadAttributes(ctx context.Context) error {
  473. if _, err := issues.LoadRepositories(ctx); err != nil {
  474. return fmt.Errorf("issue.loadAttributes: LoadRepositories: %w", err)
  475. }
  476. if err := issues.loadPosters(ctx); err != nil {
  477. return fmt.Errorf("issue.loadAttributes: loadPosters: %w", err)
  478. }
  479. if err := issues.loadLabels(ctx); err != nil {
  480. return fmt.Errorf("issue.loadAttributes: loadLabels: %w", err)
  481. }
  482. if err := issues.loadMilestones(ctx); err != nil {
  483. return fmt.Errorf("issue.loadAttributes: loadMilestones: %w", err)
  484. }
  485. if err := issues.loadProjects(ctx); err != nil {
  486. return fmt.Errorf("issue.loadAttributes: loadProjects: %w", err)
  487. }
  488. if err := issues.loadAssignees(ctx); err != nil {
  489. return fmt.Errorf("issue.loadAttributes: loadAssignees: %w", err)
  490. }
  491. if err := issues.LoadPullRequests(ctx); err != nil {
  492. return fmt.Errorf("issue.loadAttributes: loadPullRequests: %w", err)
  493. }
  494. if err := issues.loadTotalTrackedTimes(ctx); err != nil {
  495. return fmt.Errorf("issue.loadAttributes: loadTotalTrackedTimes: %w", err)
  496. }
  497. return nil
  498. }
  499. // LoadAttributes loads attributes of the issues, except for attachments and
  500. // comments
  501. func (issues IssueList) LoadAttributes() error {
  502. return issues.loadAttributes(db.DefaultContext)
  503. }
  504. // LoadComments loads comments
  505. func (issues IssueList) LoadComments(ctx context.Context) error {
  506. return issues.loadComments(ctx, builder.NewCond())
  507. }
  508. // LoadDiscussComments loads discuss comments
  509. func (issues IssueList) LoadDiscussComments(ctx context.Context) error {
  510. return issues.loadComments(ctx, builder.Eq{"comment.type": CommentTypeComment})
  511. }
  512. // GetApprovalCounts returns a map of issue ID to slice of approval counts
  513. // FIXME: only returns official counts due to double counting of non-official approvals
  514. func (issues IssueList) GetApprovalCounts(ctx context.Context) (map[int64][]*ReviewCount, error) {
  515. rCounts := make([]*ReviewCount, 0, 2*len(issues))
  516. ids := make([]int64, len(issues))
  517. for i, issue := range issues {
  518. ids[i] = issue.ID
  519. }
  520. sess := db.GetEngine(ctx).In("issue_id", ids)
  521. err := sess.Select("issue_id, type, count(id) as `count`").
  522. Where("official = ? AND dismissed = ?", true, false).
  523. GroupBy("issue_id, type").
  524. OrderBy("issue_id").
  525. Table("review").
  526. Find(&rCounts)
  527. if err != nil {
  528. return nil, err
  529. }
  530. approvalCountMap := make(map[int64][]*ReviewCount, len(issues))
  531. for _, c := range rCounts {
  532. approvalCountMap[c.IssueID] = append(approvalCountMap[c.IssueID], c)
  533. }
  534. return approvalCountMap, nil
  535. }