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.

483 lines
10 KiB

Shows total tracked time in issue and milestone list (#3341) * Show total tracked time in issue and milestone list Show total tracked time at issue page Signed-off-by: Jonas Franz <info@jonasfranz.software> * Optimizing TotalTimes by using SumInt Signed-off-by: Jonas Franz <info@jonasfranz.software> * Fixing wrong total times for milestones caused by a missing JOIN Adding unit tests for total times Signed-off-by: Jonas Franz <info@jonasfranz.software> * Logging error instead of ignoring it Signed-off-by: Jonas Franz <info@jonasfranz.software> * Correcting spelling mistakes Signed-off-by: Jonas Franz <info@jonasfranz.software> * Change error message to a short version Signed-off-by: Jonas Franz <info@jonasfranz.software> * Add error handling to TotalTimes Add variable for totalTimes Signed-off-by: Jonas Franz <info@jonasfranz.de> * Introduce TotalTrackedTimes as variable of issue Load TotalTrackedTimes by loading attributes of IssueList Load TotalTrackedTimes by loading attributes of single issue Add Sec2Time as helper to use it in templates Signed-off-by: Jonas Franz <info@jonasfranz.software> * Fixed test + gofmt Signed-off-by: Jonas Franz <info@jonasfranz.software> * Load TotalTrackedTimes via MilestoneList instead of single requests Signed-off-by: Jonas Franz <info@jonasfranz.software> * Add documentation for MilestoneList Signed-off-by: Jonas Franz <info@jonasfranz.software> * Add documentation for MilestoneList Signed-off-by: Jonas Franz <info@jonasfranz.software> * Fix test Signed-off-by: Jonas Franz <info@jonasfranz.software> * Change comment from SQL query to description Signed-off-by: Jonas Franz <info@jonasfranz.software> * Fix unit test by using int64 instead of int Signed-off-by: Jonas Franz <info@jonasfranz.software> * Fix unit test by using int64 instead of int Signed-off-by: Jonas Franz <info@jonasfranz.software> * Check if timetracker is enabled Signed-off-by: Jonas Franz <info@jonasfranz.software> * Fix test by enabling timetracking Signed-off-by: Jonas Franz <info@jonasfranz.de>
6 years ago
Shows total tracked time in issue and milestone list (#3341) * Show total tracked time in issue and milestone list Show total tracked time at issue page Signed-off-by: Jonas Franz <info@jonasfranz.software> * Optimizing TotalTimes by using SumInt Signed-off-by: Jonas Franz <info@jonasfranz.software> * Fixing wrong total times for milestones caused by a missing JOIN Adding unit tests for total times Signed-off-by: Jonas Franz <info@jonasfranz.software> * Logging error instead of ignoring it Signed-off-by: Jonas Franz <info@jonasfranz.software> * Correcting spelling mistakes Signed-off-by: Jonas Franz <info@jonasfranz.software> * Change error message to a short version Signed-off-by: Jonas Franz <info@jonasfranz.software> * Add error handling to TotalTimes Add variable for totalTimes Signed-off-by: Jonas Franz <info@jonasfranz.de> * Introduce TotalTrackedTimes as variable of issue Load TotalTrackedTimes by loading attributes of IssueList Load TotalTrackedTimes by loading attributes of single issue Add Sec2Time as helper to use it in templates Signed-off-by: Jonas Franz <info@jonasfranz.software> * Fixed test + gofmt Signed-off-by: Jonas Franz <info@jonasfranz.software> * Load TotalTrackedTimes via MilestoneList instead of single requests Signed-off-by: Jonas Franz <info@jonasfranz.software> * Add documentation for MilestoneList Signed-off-by: Jonas Franz <info@jonasfranz.software> * Add documentation for MilestoneList Signed-off-by: Jonas Franz <info@jonasfranz.software> * Fix test Signed-off-by: Jonas Franz <info@jonasfranz.software> * Change comment from SQL query to description Signed-off-by: Jonas Franz <info@jonasfranz.software> * Fix unit test by using int64 instead of int Signed-off-by: Jonas Franz <info@jonasfranz.software> * Fix unit test by using int64 instead of int Signed-off-by: Jonas Franz <info@jonasfranz.software> * Check if timetracker is enabled Signed-off-by: Jonas Franz <info@jonasfranz.software> * Fix test by enabling timetracking Signed-off-by: Jonas Franz <info@jonasfranz.de>
6 years ago
Shows total tracked time in issue and milestone list (#3341) * Show total tracked time in issue and milestone list Show total tracked time at issue page Signed-off-by: Jonas Franz <info@jonasfranz.software> * Optimizing TotalTimes by using SumInt Signed-off-by: Jonas Franz <info@jonasfranz.software> * Fixing wrong total times for milestones caused by a missing JOIN Adding unit tests for total times Signed-off-by: Jonas Franz <info@jonasfranz.software> * Logging error instead of ignoring it Signed-off-by: Jonas Franz <info@jonasfranz.software> * Correcting spelling mistakes Signed-off-by: Jonas Franz <info@jonasfranz.software> * Change error message to a short version Signed-off-by: Jonas Franz <info@jonasfranz.software> * Add error handling to TotalTimes Add variable for totalTimes Signed-off-by: Jonas Franz <info@jonasfranz.de> * Introduce TotalTrackedTimes as variable of issue Load TotalTrackedTimes by loading attributes of IssueList Load TotalTrackedTimes by loading attributes of single issue Add Sec2Time as helper to use it in templates Signed-off-by: Jonas Franz <info@jonasfranz.software> * Fixed test + gofmt Signed-off-by: Jonas Franz <info@jonasfranz.software> * Load TotalTrackedTimes via MilestoneList instead of single requests Signed-off-by: Jonas Franz <info@jonasfranz.software> * Add documentation for MilestoneList Signed-off-by: Jonas Franz <info@jonasfranz.software> * Add documentation for MilestoneList Signed-off-by: Jonas Franz <info@jonasfranz.software> * Fix test Signed-off-by: Jonas Franz <info@jonasfranz.software> * Change comment from SQL query to description Signed-off-by: Jonas Franz <info@jonasfranz.software> * Fix unit test by using int64 instead of int Signed-off-by: Jonas Franz <info@jonasfranz.software> * Fix unit test by using int64 instead of int Signed-off-by: Jonas Franz <info@jonasfranz.software> * Check if timetracker is enabled Signed-off-by: Jonas Franz <info@jonasfranz.software> * Fix test by enabling timetracking Signed-off-by: Jonas Franz <info@jonasfranz.de>
6 years ago
Shows total tracked time in issue and milestone list (#3341) * Show total tracked time in issue and milestone list Show total tracked time at issue page Signed-off-by: Jonas Franz <info@jonasfranz.software> * Optimizing TotalTimes by using SumInt Signed-off-by: Jonas Franz <info@jonasfranz.software> * Fixing wrong total times for milestones caused by a missing JOIN Adding unit tests for total times Signed-off-by: Jonas Franz <info@jonasfranz.software> * Logging error instead of ignoring it Signed-off-by: Jonas Franz <info@jonasfranz.software> * Correcting spelling mistakes Signed-off-by: Jonas Franz <info@jonasfranz.software> * Change error message to a short version Signed-off-by: Jonas Franz <info@jonasfranz.software> * Add error handling to TotalTimes Add variable for totalTimes Signed-off-by: Jonas Franz <info@jonasfranz.de> * Introduce TotalTrackedTimes as variable of issue Load TotalTrackedTimes by loading attributes of IssueList Load TotalTrackedTimes by loading attributes of single issue Add Sec2Time as helper to use it in templates Signed-off-by: Jonas Franz <info@jonasfranz.software> * Fixed test + gofmt Signed-off-by: Jonas Franz <info@jonasfranz.software> * Load TotalTrackedTimes via MilestoneList instead of single requests Signed-off-by: Jonas Franz <info@jonasfranz.software> * Add documentation for MilestoneList Signed-off-by: Jonas Franz <info@jonasfranz.software> * Add documentation for MilestoneList Signed-off-by: Jonas Franz <info@jonasfranz.software> * Fix test Signed-off-by: Jonas Franz <info@jonasfranz.software> * Change comment from SQL query to description Signed-off-by: Jonas Franz <info@jonasfranz.software> * Fix unit test by using int64 instead of int Signed-off-by: Jonas Franz <info@jonasfranz.software> * Fix unit test by using int64 instead of int Signed-off-by: Jonas Franz <info@jonasfranz.software> * Check if timetracker is enabled Signed-off-by: Jonas Franz <info@jonasfranz.software> * Fix test by enabling timetracking Signed-off-by: Jonas Franz <info@jonasfranz.de>
6 years ago
Shows total tracked time in issue and milestone list (#3341) * Show total tracked time in issue and milestone list Show total tracked time at issue page Signed-off-by: Jonas Franz <info@jonasfranz.software> * Optimizing TotalTimes by using SumInt Signed-off-by: Jonas Franz <info@jonasfranz.software> * Fixing wrong total times for milestones caused by a missing JOIN Adding unit tests for total times Signed-off-by: Jonas Franz <info@jonasfranz.software> * Logging error instead of ignoring it Signed-off-by: Jonas Franz <info@jonasfranz.software> * Correcting spelling mistakes Signed-off-by: Jonas Franz <info@jonasfranz.software> * Change error message to a short version Signed-off-by: Jonas Franz <info@jonasfranz.software> * Add error handling to TotalTimes Add variable for totalTimes Signed-off-by: Jonas Franz <info@jonasfranz.de> * Introduce TotalTrackedTimes as variable of issue Load TotalTrackedTimes by loading attributes of IssueList Load TotalTrackedTimes by loading attributes of single issue Add Sec2Time as helper to use it in templates Signed-off-by: Jonas Franz <info@jonasfranz.software> * Fixed test + gofmt Signed-off-by: Jonas Franz <info@jonasfranz.software> * Load TotalTrackedTimes via MilestoneList instead of single requests Signed-off-by: Jonas Franz <info@jonasfranz.software> * Add documentation for MilestoneList Signed-off-by: Jonas Franz <info@jonasfranz.software> * Add documentation for MilestoneList Signed-off-by: Jonas Franz <info@jonasfranz.software> * Fix test Signed-off-by: Jonas Franz <info@jonasfranz.software> * Change comment from SQL query to description Signed-off-by: Jonas Franz <info@jonasfranz.software> * Fix unit test by using int64 instead of int Signed-off-by: Jonas Franz <info@jonasfranz.software> * Fix unit test by using int64 instead of int Signed-off-by: Jonas Franz <info@jonasfranz.software> * Check if timetracker is enabled Signed-off-by: Jonas Franz <info@jonasfranz.software> * Fix test by enabling timetracking Signed-off-by: Jonas Franz <info@jonasfranz.de>
6 years ago
  1. // Copyright 2017 The Gitea Authors. All rights reserved.
  2. // Use of this source code is governed by a MIT-style
  3. // license that can be found in the LICENSE file.
  4. package models
  5. import "fmt"
  6. // IssueList defines a list of issues
  7. type IssueList []*Issue
  8. const (
  9. // default variables number on IN () in SQL
  10. defaultMaxInSize = 50
  11. )
  12. func (issues IssueList) getRepoIDs() []int64 {
  13. repoIDs := make(map[int64]struct{}, len(issues))
  14. for _, issue := range issues {
  15. if _, ok := repoIDs[issue.RepoID]; !ok {
  16. repoIDs[issue.RepoID] = struct{}{}
  17. }
  18. }
  19. return keysInt64(repoIDs)
  20. }
  21. func (issues IssueList) loadRepositories(e Engine) ([]*Repository, error) {
  22. if len(issues) == 0 {
  23. return nil, nil
  24. }
  25. repoIDs := issues.getRepoIDs()
  26. repoMaps := make(map[int64]*Repository, len(repoIDs))
  27. var left = len(repoIDs)
  28. for left > 0 {
  29. var limit = defaultMaxInSize
  30. if left < limit {
  31. limit = left
  32. }
  33. err := e.
  34. In("id", repoIDs[:limit]).
  35. Find(&repoMaps)
  36. if err != nil {
  37. return nil, fmt.Errorf("find repository: %v", err)
  38. }
  39. left = left - limit
  40. repoIDs = repoIDs[limit:]
  41. }
  42. for _, issue := range issues {
  43. issue.Repo = repoMaps[issue.RepoID]
  44. }
  45. return valuesRepository(repoMaps), nil
  46. }
  47. // LoadRepositories loads issues' all repositories
  48. func (issues IssueList) LoadRepositories() ([]*Repository, error) {
  49. return issues.loadRepositories(x)
  50. }
  51. func (issues IssueList) getPosterIDs() []int64 {
  52. posterIDs := make(map[int64]struct{}, len(issues))
  53. for _, issue := range issues {
  54. if _, ok := posterIDs[issue.PosterID]; !ok {
  55. posterIDs[issue.PosterID] = struct{}{}
  56. }
  57. }
  58. return keysInt64(posterIDs)
  59. }
  60. func (issues IssueList) loadPosters(e Engine) error {
  61. if len(issues) == 0 {
  62. return nil
  63. }
  64. posterIDs := issues.getPosterIDs()
  65. posterMaps := make(map[int64]*User, len(posterIDs))
  66. var left = len(posterIDs)
  67. for left > 0 {
  68. var limit = defaultMaxInSize
  69. if left < limit {
  70. limit = left
  71. }
  72. err := e.
  73. In("id", posterIDs[:limit]).
  74. Find(&posterMaps)
  75. if err != nil {
  76. return err
  77. }
  78. left = left - limit
  79. posterIDs = posterIDs[limit:]
  80. }
  81. for _, issue := range issues {
  82. if issue.PosterID <= 0 {
  83. continue
  84. }
  85. var ok bool
  86. if issue.Poster, ok = posterMaps[issue.PosterID]; !ok {
  87. issue.Poster = NewGhostUser()
  88. }
  89. }
  90. return nil
  91. }
  92. func (issues IssueList) getIssueIDs() []int64 {
  93. var ids = make([]int64, 0, len(issues))
  94. for _, issue := range issues {
  95. ids = append(ids, issue.ID)
  96. }
  97. return ids
  98. }
  99. func (issues IssueList) loadLabels(e Engine) error {
  100. if len(issues) == 0 {
  101. return nil
  102. }
  103. type LabelIssue struct {
  104. Label *Label `xorm:"extends"`
  105. IssueLabel *IssueLabel `xorm:"extends"`
  106. }
  107. var issueLabels = make(map[int64][]*Label, len(issues)*3)
  108. var issueIDs = issues.getIssueIDs()
  109. var left = len(issueIDs)
  110. for left > 0 {
  111. var limit = defaultMaxInSize
  112. if left < limit {
  113. limit = left
  114. }
  115. rows, err := e.Table("label").
  116. Join("LEFT", "issue_label", "issue_label.label_id = label.id").
  117. In("issue_label.issue_id", issueIDs[:limit]).
  118. Asc("label.name").
  119. Rows(new(LabelIssue))
  120. if err != nil {
  121. return err
  122. }
  123. for rows.Next() {
  124. var labelIssue LabelIssue
  125. err = rows.Scan(&labelIssue)
  126. if err != nil {
  127. rows.Close()
  128. return err
  129. }
  130. issueLabels[labelIssue.IssueLabel.IssueID] = append(issueLabels[labelIssue.IssueLabel.IssueID], labelIssue.Label)
  131. }
  132. rows.Close()
  133. left = left - limit
  134. issueIDs = issueIDs[limit:]
  135. }
  136. for _, issue := range issues {
  137. issue.Labels = issueLabels[issue.ID]
  138. }
  139. return nil
  140. }
  141. func (issues IssueList) getMilestoneIDs() []int64 {
  142. var ids = make(map[int64]struct{}, len(issues))
  143. for _, issue := range issues {
  144. if _, ok := ids[issue.MilestoneID]; !ok {
  145. ids[issue.MilestoneID] = struct{}{}
  146. }
  147. }
  148. return keysInt64(ids)
  149. }
  150. func (issues IssueList) loadMilestones(e Engine) error {
  151. milestoneIDs := issues.getMilestoneIDs()
  152. if len(milestoneIDs) == 0 {
  153. return nil
  154. }
  155. milestoneMaps := make(map[int64]*Milestone, len(milestoneIDs))
  156. var left = len(milestoneIDs)
  157. for left > 0 {
  158. var limit = defaultMaxInSize
  159. if left < limit {
  160. limit = left
  161. }
  162. err := e.
  163. In("id", milestoneIDs[:limit]).
  164. Find(&milestoneMaps)
  165. if err != nil {
  166. return err
  167. }
  168. left = left - limit
  169. milestoneIDs = milestoneIDs[limit:]
  170. }
  171. for _, issue := range issues {
  172. issue.Milestone = milestoneMaps[issue.MilestoneID]
  173. }
  174. return nil
  175. }
  176. func (issues IssueList) loadAssignees(e Engine) error {
  177. if len(issues) == 0 {
  178. return nil
  179. }
  180. type AssigneeIssue struct {
  181. IssueAssignee *IssueAssignees `xorm:"extends"`
  182. Assignee *User `xorm:"extends"`
  183. }
  184. var assignees = make(map[int64][]*User, len(issues))
  185. var issueIDs = issues.getIssueIDs()
  186. var left = len(issueIDs)
  187. for left > 0 {
  188. var limit = defaultMaxInSize
  189. if left < limit {
  190. limit = left
  191. }
  192. rows, err := e.Table("issue_assignees").
  193. Join("INNER", "`user`", "`user`.id = `issue_assignees`.assignee_id").
  194. In("`issue_assignees`.issue_id", issueIDs[:limit]).
  195. Rows(new(AssigneeIssue))
  196. if err != nil {
  197. return err
  198. }
  199. for rows.Next() {
  200. var assigneeIssue AssigneeIssue
  201. err = rows.Scan(&assigneeIssue)
  202. if err != nil {
  203. rows.Close()
  204. return err
  205. }
  206. assignees[assigneeIssue.IssueAssignee.IssueID] = append(assignees[assigneeIssue.IssueAssignee.IssueID], assigneeIssue.Assignee)
  207. }
  208. rows.Close()
  209. left = left - limit
  210. issueIDs = issueIDs[limit:]
  211. }
  212. for _, issue := range issues {
  213. issue.Assignees = assignees[issue.ID]
  214. }
  215. return nil
  216. }
  217. func (issues IssueList) getPullIssueIDs() []int64 {
  218. var ids = make([]int64, 0, len(issues))
  219. for _, issue := range issues {
  220. if issue.IsPull && issue.PullRequest == nil {
  221. ids = append(ids, issue.ID)
  222. }
  223. }
  224. return ids
  225. }
  226. func (issues IssueList) loadPullRequests(e Engine) error {
  227. issuesIDs := issues.getPullIssueIDs()
  228. if len(issuesIDs) == 0 {
  229. return nil
  230. }
  231. pullRequestMaps := make(map[int64]*PullRequest, len(issuesIDs))
  232. var left = len(issuesIDs)
  233. for left > 0 {
  234. var limit = defaultMaxInSize
  235. if left < limit {
  236. limit = left
  237. }
  238. rows, err := e.
  239. In("issue_id", issuesIDs[:limit]).
  240. Rows(new(PullRequest))
  241. if err != nil {
  242. return err
  243. }
  244. for rows.Next() {
  245. var pr PullRequest
  246. err = rows.Scan(&pr)
  247. if err != nil {
  248. rows.Close()
  249. return err
  250. }
  251. pullRequestMaps[pr.IssueID] = &pr
  252. }
  253. rows.Close()
  254. left = left - limit
  255. issuesIDs = issuesIDs[limit:]
  256. }
  257. for _, issue := range issues {
  258. issue.PullRequest = pullRequestMaps[issue.ID]
  259. }
  260. return nil
  261. }
  262. func (issues IssueList) loadAttachments(e Engine) (err error) {
  263. if len(issues) == 0 {
  264. return nil
  265. }
  266. var attachments = make(map[int64][]*Attachment, len(issues))
  267. var issuesIDs = issues.getIssueIDs()
  268. var left = len(issuesIDs)
  269. for left > 0 {
  270. var limit = defaultMaxInSize
  271. if left < limit {
  272. limit = left
  273. }
  274. rows, err := e.Table("attachment").
  275. Join("INNER", "issue", "issue.id = attachment.issue_id").
  276. In("issue.id", issuesIDs[:limit]).
  277. Rows(new(Attachment))
  278. if err != nil {
  279. return err
  280. }
  281. for rows.Next() {
  282. var attachment Attachment
  283. err = rows.Scan(&attachment)
  284. if err != nil {
  285. rows.Close()
  286. return err
  287. }
  288. attachments[attachment.IssueID] = append(attachments[attachment.IssueID], &attachment)
  289. }
  290. rows.Close()
  291. left = left - limit
  292. issuesIDs = issuesIDs[limit:]
  293. }
  294. for _, issue := range issues {
  295. issue.Attachments = attachments[issue.ID]
  296. }
  297. return nil
  298. }
  299. func (issues IssueList) loadComments(e Engine) (err error) {
  300. if len(issues) == 0 {
  301. return nil
  302. }
  303. var comments = make(map[int64][]*Comment, len(issues))
  304. var issuesIDs = issues.getIssueIDs()
  305. var left = len(issuesIDs)
  306. for left > 0 {
  307. var limit = defaultMaxInSize
  308. if left < limit {
  309. limit = left
  310. }
  311. rows, err := e.Table("comment").
  312. Join("INNER", "issue", "issue.id = comment.issue_id").
  313. In("issue.id", issuesIDs[:limit]).
  314. Rows(new(Comment))
  315. if err != nil {
  316. return err
  317. }
  318. for rows.Next() {
  319. var comment Comment
  320. err = rows.Scan(&comment)
  321. if err != nil {
  322. rows.Close()
  323. return err
  324. }
  325. comments[comment.IssueID] = append(comments[comment.IssueID], &comment)
  326. }
  327. rows.Close()
  328. left = left - limit
  329. issuesIDs = issuesIDs[limit:]
  330. }
  331. for _, issue := range issues {
  332. issue.Comments = comments[issue.ID]
  333. }
  334. return nil
  335. }
  336. func (issues IssueList) loadTotalTrackedTimes(e Engine) (err error) {
  337. type totalTimesByIssue struct {
  338. IssueID int64
  339. Time int64
  340. }
  341. if len(issues) == 0 {
  342. return nil
  343. }
  344. var trackedTimes = make(map[int64]int64, len(issues))
  345. var ids = make([]int64, 0, len(issues))
  346. for _, issue := range issues {
  347. if issue.Repo.IsTimetrackerEnabled() {
  348. ids = append(ids, issue.ID)
  349. }
  350. }
  351. var left = len(ids)
  352. for left > 0 {
  353. var limit = defaultMaxInSize
  354. if left < limit {
  355. limit = left
  356. }
  357. // select issue_id, sum(time) from tracked_time where issue_id in (<issue ids in current page>) group by issue_id
  358. rows, err := e.Table("tracked_time").
  359. Select("issue_id, sum(time) as time").
  360. In("issue_id", ids[:limit]).
  361. GroupBy("issue_id").
  362. Rows(new(totalTimesByIssue))
  363. if err != nil {
  364. return err
  365. }
  366. for rows.Next() {
  367. var totalTime totalTimesByIssue
  368. err = rows.Scan(&totalTime)
  369. if err != nil {
  370. rows.Close()
  371. return err
  372. }
  373. trackedTimes[totalTime.IssueID] = totalTime.Time
  374. }
  375. rows.Close()
  376. left = left - limit
  377. ids = ids[limit:]
  378. }
  379. for _, issue := range issues {
  380. issue.TotalTrackedTime = trackedTimes[issue.ID]
  381. }
  382. return nil
  383. }
  384. // loadAttributes loads all attributes, expect for attachments and comments
  385. func (issues IssueList) loadAttributes(e Engine) (err error) {
  386. if _, err = issues.loadRepositories(e); err != nil {
  387. return
  388. }
  389. if err = issues.loadPosters(e); err != nil {
  390. return
  391. }
  392. if err = issues.loadLabels(e); err != nil {
  393. return
  394. }
  395. if err = issues.loadMilestones(e); err != nil {
  396. return
  397. }
  398. if err = issues.loadAssignees(e); err != nil {
  399. return
  400. }
  401. if err = issues.loadPullRequests(e); err != nil {
  402. return
  403. }
  404. if err = issues.loadTotalTrackedTimes(e); err != nil {
  405. return
  406. }
  407. return nil
  408. }
  409. // LoadAttributes loads attributes of the issues, except for attachments and
  410. // comments
  411. func (issues IssueList) LoadAttributes() error {
  412. return issues.loadAttributes(x)
  413. }
  414. // LoadAttachments loads attachments
  415. func (issues IssueList) LoadAttachments() error {
  416. return issues.loadAttachments(x)
  417. }
  418. // LoadComments loads comments
  419. func (issues IssueList) LoadComments() error {
  420. return issues.loadComments(x)
  421. }