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