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