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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633
  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.RepositoryList, 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) LoadProjects(ctx context.Context) error {
  207. issueIDs := issues.getIssueIDs()
  208. projectMaps := make(map[int64]*project_model.Project, len(issues))
  209. left := len(issueIDs)
  210. type projectWithIssueID struct {
  211. *project_model.Project `xorm:"extends"`
  212. IssueID int64
  213. }
  214. for left > 0 {
  215. limit := db.DefaultMaxInSize
  216. if left < limit {
  217. limit = left
  218. }
  219. projects := make([]*projectWithIssueID, 0, limit)
  220. err := db.GetEngine(ctx).
  221. Table("project").
  222. Select("project.*, project_issue.issue_id").
  223. Join("INNER", "project_issue", "project.id = project_issue.project_id").
  224. In("project_issue.issue_id", issueIDs[:limit]).
  225. Find(&projects)
  226. if err != nil {
  227. return err
  228. }
  229. for _, project := range projects {
  230. projectMaps[project.IssueID] = project.Project
  231. }
  232. left -= limit
  233. issueIDs = issueIDs[limit:]
  234. }
  235. for _, issue := range issues {
  236. issue.Project = projectMaps[issue.ID]
  237. }
  238. return nil
  239. }
  240. func (issues IssueList) loadAssignees(ctx context.Context) error {
  241. if len(issues) == 0 {
  242. return nil
  243. }
  244. type AssigneeIssue struct {
  245. IssueAssignee *IssueAssignees `xorm:"extends"`
  246. Assignee *user_model.User `xorm:"extends"`
  247. }
  248. assignees := make(map[int64][]*user_model.User, len(issues))
  249. issueIDs := issues.getIssueIDs()
  250. left := len(issueIDs)
  251. for left > 0 {
  252. limit := db.DefaultMaxInSize
  253. if left < limit {
  254. limit = left
  255. }
  256. rows, err := db.GetEngine(ctx).Table("issue_assignees").
  257. Join("INNER", "`user`", "`user`.id = `issue_assignees`.assignee_id").
  258. In("`issue_assignees`.issue_id", issueIDs[:limit]).OrderBy(user_model.GetOrderByName()).
  259. Rows(new(AssigneeIssue))
  260. if err != nil {
  261. return err
  262. }
  263. for rows.Next() {
  264. var assigneeIssue AssigneeIssue
  265. err = rows.Scan(&assigneeIssue)
  266. if err != nil {
  267. if err1 := rows.Close(); err1 != nil {
  268. return fmt.Errorf("IssueList.loadAssignees: Close: %w", err1)
  269. }
  270. return err
  271. }
  272. assignees[assigneeIssue.IssueAssignee.IssueID] = append(assignees[assigneeIssue.IssueAssignee.IssueID], assigneeIssue.Assignee)
  273. }
  274. if err1 := rows.Close(); err1 != nil {
  275. return fmt.Errorf("IssueList.loadAssignees: Close: %w", err1)
  276. }
  277. left -= limit
  278. issueIDs = issueIDs[limit:]
  279. }
  280. for _, issue := range issues {
  281. issue.Assignees = assignees[issue.ID]
  282. }
  283. return nil
  284. }
  285. func (issues IssueList) getPullIssueIDs() []int64 {
  286. ids := make([]int64, 0, len(issues))
  287. for _, issue := range issues {
  288. if issue.IsPull && issue.PullRequest == nil {
  289. ids = append(ids, issue.ID)
  290. }
  291. }
  292. return ids
  293. }
  294. // LoadPullRequests loads pull requests
  295. func (issues IssueList) LoadPullRequests(ctx context.Context) error {
  296. issuesIDs := issues.getPullIssueIDs()
  297. if len(issuesIDs) == 0 {
  298. return nil
  299. }
  300. pullRequestMaps := make(map[int64]*PullRequest, len(issuesIDs))
  301. left := len(issuesIDs)
  302. for left > 0 {
  303. limit := db.DefaultMaxInSize
  304. if left < limit {
  305. limit = left
  306. }
  307. rows, err := db.GetEngine(ctx).
  308. In("issue_id", issuesIDs[:limit]).
  309. Rows(new(PullRequest))
  310. if err != nil {
  311. return err
  312. }
  313. for rows.Next() {
  314. var pr PullRequest
  315. err = rows.Scan(&pr)
  316. if err != nil {
  317. if err1 := rows.Close(); err1 != nil {
  318. return fmt.Errorf("IssueList.loadPullRequests: Close: %w", err1)
  319. }
  320. return err
  321. }
  322. pullRequestMaps[pr.IssueID] = &pr
  323. }
  324. if err1 := rows.Close(); err1 != nil {
  325. return fmt.Errorf("IssueList.loadPullRequests: Close: %w", err1)
  326. }
  327. left -= limit
  328. issuesIDs = issuesIDs[limit:]
  329. }
  330. for _, issue := range issues {
  331. issue.PullRequest = pullRequestMaps[issue.ID]
  332. if issue.PullRequest != nil {
  333. issue.PullRequest.Issue = issue
  334. }
  335. }
  336. return nil
  337. }
  338. // LoadAttachments loads attachments
  339. func (issues IssueList) LoadAttachments(ctx context.Context) (err error) {
  340. if len(issues) == 0 {
  341. return nil
  342. }
  343. attachments := make(map[int64][]*repo_model.Attachment, len(issues))
  344. issuesIDs := issues.getIssueIDs()
  345. left := len(issuesIDs)
  346. for left > 0 {
  347. limit := db.DefaultMaxInSize
  348. if left < limit {
  349. limit = left
  350. }
  351. rows, err := db.GetEngine(ctx).
  352. In("issue_id", issuesIDs[:limit]).
  353. Rows(new(repo_model.Attachment))
  354. if err != nil {
  355. return err
  356. }
  357. for rows.Next() {
  358. var attachment repo_model.Attachment
  359. err = rows.Scan(&attachment)
  360. if err != nil {
  361. if err1 := rows.Close(); err1 != nil {
  362. return fmt.Errorf("IssueList.loadAttachments: Close: %w", err1)
  363. }
  364. return err
  365. }
  366. attachments[attachment.IssueID] = append(attachments[attachment.IssueID], &attachment)
  367. }
  368. if err1 := rows.Close(); err1 != nil {
  369. return fmt.Errorf("IssueList.loadAttachments: Close: %w", err1)
  370. }
  371. left -= limit
  372. issuesIDs = issuesIDs[limit:]
  373. }
  374. for _, issue := range issues {
  375. issue.Attachments = attachments[issue.ID]
  376. }
  377. return nil
  378. }
  379. func (issues IssueList) loadComments(ctx context.Context, cond builder.Cond) (err error) {
  380. if len(issues) == 0 {
  381. return nil
  382. }
  383. comments := make(map[int64][]*Comment, len(issues))
  384. issuesIDs := issues.getIssueIDs()
  385. left := len(issuesIDs)
  386. for left > 0 {
  387. limit := db.DefaultMaxInSize
  388. if left < limit {
  389. limit = left
  390. }
  391. rows, err := db.GetEngine(ctx).Table("comment").
  392. Join("INNER", "issue", "issue.id = comment.issue_id").
  393. In("issue.id", issuesIDs[:limit]).
  394. Where(cond).
  395. Rows(new(Comment))
  396. if err != nil {
  397. return err
  398. }
  399. for rows.Next() {
  400. var comment Comment
  401. err = rows.Scan(&comment)
  402. if err != nil {
  403. if err1 := rows.Close(); err1 != nil {
  404. return fmt.Errorf("IssueList.loadComments: Close: %w", err1)
  405. }
  406. return err
  407. }
  408. comments[comment.IssueID] = append(comments[comment.IssueID], &comment)
  409. }
  410. if err1 := rows.Close(); err1 != nil {
  411. return fmt.Errorf("IssueList.loadComments: Close: %w", err1)
  412. }
  413. left -= limit
  414. issuesIDs = issuesIDs[limit:]
  415. }
  416. for _, issue := range issues {
  417. issue.Comments = comments[issue.ID]
  418. }
  419. return nil
  420. }
  421. func (issues IssueList) loadTotalTrackedTimes(ctx context.Context) (err error) {
  422. type totalTimesByIssue struct {
  423. IssueID int64
  424. Time int64
  425. }
  426. if len(issues) == 0 {
  427. return nil
  428. }
  429. trackedTimes := make(map[int64]int64, len(issues))
  430. reposMap := make(map[int64]*repo_model.Repository, len(issues))
  431. for _, issue := range issues {
  432. reposMap[issue.RepoID] = issue.Repo
  433. }
  434. repos := repo_model.RepositoryListOfMap(reposMap)
  435. if err := repos.LoadUnits(ctx); err != nil {
  436. return err
  437. }
  438. ids := make([]int64, 0, len(issues))
  439. for _, issue := range issues {
  440. if issue.Repo.IsTimetrackerEnabled(ctx) {
  441. ids = append(ids, issue.ID)
  442. }
  443. }
  444. left := len(ids)
  445. for left > 0 {
  446. limit := db.DefaultMaxInSize
  447. if left < limit {
  448. limit = left
  449. }
  450. // select issue_id, sum(time) from tracked_time where issue_id in (<issue ids in current page>) group by issue_id
  451. rows, err := db.GetEngine(ctx).Table("tracked_time").
  452. Where("deleted = ?", false).
  453. Select("issue_id, sum(time) as time").
  454. In("issue_id", ids[:limit]).
  455. GroupBy("issue_id").
  456. Rows(new(totalTimesByIssue))
  457. if err != nil {
  458. return err
  459. }
  460. for rows.Next() {
  461. var totalTime totalTimesByIssue
  462. err = rows.Scan(&totalTime)
  463. if err != nil {
  464. if err1 := rows.Close(); err1 != nil {
  465. return fmt.Errorf("IssueList.loadTotalTrackedTimes: Close: %w", err1)
  466. }
  467. return err
  468. }
  469. trackedTimes[totalTime.IssueID] = totalTime.Time
  470. }
  471. if err1 := rows.Close(); err1 != nil {
  472. return fmt.Errorf("IssueList.loadTotalTrackedTimes: Close: %w", err1)
  473. }
  474. left -= limit
  475. ids = ids[limit:]
  476. }
  477. for _, issue := range issues {
  478. issue.TotalTrackedTime = trackedTimes[issue.ID]
  479. }
  480. return nil
  481. }
  482. // loadAttributes loads all attributes, expect for attachments and comments
  483. func (issues IssueList) LoadAttributes(ctx context.Context) error {
  484. if _, err := issues.LoadRepositories(ctx); err != nil {
  485. return fmt.Errorf("issue.loadAttributes: LoadRepositories: %w", err)
  486. }
  487. if err := issues.loadPosters(ctx); err != nil {
  488. return fmt.Errorf("issue.loadAttributes: loadPosters: %w", err)
  489. }
  490. if err := issues.loadLabels(ctx); err != nil {
  491. return fmt.Errorf("issue.loadAttributes: loadLabels: %w", err)
  492. }
  493. if err := issues.loadMilestones(ctx); err != nil {
  494. return fmt.Errorf("issue.loadAttributes: loadMilestones: %w", err)
  495. }
  496. if err := issues.LoadProjects(ctx); err != nil {
  497. return fmt.Errorf("issue.loadAttributes: loadProjects: %w", err)
  498. }
  499. if err := issues.loadAssignees(ctx); err != nil {
  500. return fmt.Errorf("issue.loadAttributes: loadAssignees: %w", err)
  501. }
  502. if err := issues.LoadPullRequests(ctx); err != nil {
  503. return fmt.Errorf("issue.loadAttributes: loadPullRequests: %w", err)
  504. }
  505. if err := issues.loadTotalTrackedTimes(ctx); err != nil {
  506. return fmt.Errorf("issue.loadAttributes: loadTotalTrackedTimes: %w", err)
  507. }
  508. return nil
  509. }
  510. // LoadComments loads comments
  511. func (issues IssueList) LoadComments(ctx context.Context) error {
  512. return issues.loadComments(ctx, builder.NewCond())
  513. }
  514. // LoadDiscussComments loads discuss comments
  515. func (issues IssueList) LoadDiscussComments(ctx context.Context) error {
  516. return issues.loadComments(ctx, builder.Eq{"comment.type": CommentTypeComment})
  517. }
  518. // GetApprovalCounts returns a map of issue ID to slice of approval counts
  519. // FIXME: only returns official counts due to double counting of non-official approvals
  520. func (issues IssueList) GetApprovalCounts(ctx context.Context) (map[int64][]*ReviewCount, error) {
  521. rCounts := make([]*ReviewCount, 0, 2*len(issues))
  522. ids := make([]int64, len(issues))
  523. for i, issue := range issues {
  524. ids[i] = issue.ID
  525. }
  526. sess := db.GetEngine(ctx).In("issue_id", ids)
  527. err := sess.Select("issue_id, type, count(id) as `count`").
  528. Where("official = ? AND dismissed = ?", true, false).
  529. GroupBy("issue_id, type").
  530. OrderBy("issue_id").
  531. Table("review").
  532. Find(&rCounts)
  533. if err != nil {
  534. return nil, err
  535. }
  536. approvalCountMap := make(map[int64][]*ReviewCount, len(issues))
  537. for _, c := range rCounts {
  538. approvalCountMap[c.IssueID] = append(approvalCountMap[c.IssueID], c)
  539. }
  540. return approvalCountMap, nil
  541. }
  542. func (issues IssueList) LoadIsRead(ctx context.Context, userID int64) error {
  543. issueIDs := issues.getIssueIDs()
  544. issueUsers := make([]*IssueUser, 0, len(issueIDs))
  545. if err := db.GetEngine(ctx).Where("uid =?", userID).
  546. In("issue_id").
  547. Find(&issueUsers); err != nil {
  548. return err
  549. }
  550. for _, issueUser := range issueUsers {
  551. for _, issue := range issues {
  552. if issue.ID == issueUser.IssueID {
  553. issue.IsRead = issueUser.IsRead
  554. }
  555. }
  556. }
  557. return nil
  558. }