summaryrefslogtreecommitdiffstats
path: root/models/issues/issue_list.go
diff options
context:
space:
mode:
Diffstat (limited to 'models/issues/issue_list.go')
-rw-r--r--models/issues/issue_list.go565
1 files changed, 565 insertions, 0 deletions
diff --git a/models/issues/issue_list.go b/models/issues/issue_list.go
new file mode 100644
index 0000000000..20e9949b66
--- /dev/null
+++ b/models/issues/issue_list.go
@@ -0,0 +1,565 @@
+// Copyright 2017 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package issues
+
+import (
+ "context"
+ "fmt"
+
+ "code.gitea.io/gitea/models/db"
+ repo_model "code.gitea.io/gitea/models/repo"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/container"
+
+ "xorm.io/builder"
+)
+
+// IssueList defines a list of issues
+type IssueList []*Issue
+
+// get the repo IDs to be loaded later, these IDs are for issue.Repo and issue.PullRequest.HeadRepo
+func (issues IssueList) getRepoIDs() []int64 {
+ repoIDs := make(map[int64]struct{}, len(issues))
+ for _, issue := range issues {
+ if issue.Repo == nil {
+ repoIDs[issue.RepoID] = struct{}{}
+ }
+ if issue.PullRequest != nil && issue.PullRequest.HeadRepo == nil {
+ repoIDs[issue.PullRequest.HeadRepoID] = struct{}{}
+ }
+ }
+ return container.KeysInt64(repoIDs)
+}
+
+func (issues IssueList) loadRepositories(ctx context.Context) ([]*repo_model.Repository, error) {
+ if len(issues) == 0 {
+ return nil, nil
+ }
+
+ repoIDs := issues.getRepoIDs()
+ repoMaps := make(map[int64]*repo_model.Repository, len(repoIDs))
+ left := len(repoIDs)
+ for left > 0 {
+ limit := db.DefaultMaxInSize
+ if left < limit {
+ limit = left
+ }
+ err := db.GetEngine(ctx).
+ In("id", repoIDs[:limit]).
+ Find(&repoMaps)
+ if err != nil {
+ return nil, fmt.Errorf("find repository: %v", err)
+ }
+ left -= limit
+ repoIDs = repoIDs[limit:]
+ }
+
+ for _, issue := range issues {
+ if issue.Repo == nil {
+ issue.Repo = repoMaps[issue.RepoID]
+ } else {
+ repoMaps[issue.RepoID] = issue.Repo
+ }
+ if issue.PullRequest != nil {
+ issue.PullRequest.BaseRepo = issue.Repo
+ if issue.PullRequest.HeadRepo == nil {
+ issue.PullRequest.HeadRepo = repoMaps[issue.PullRequest.HeadRepoID]
+ }
+ }
+ }
+ return repo_model.ValuesRepository(repoMaps), nil
+}
+
+// LoadRepositories loads issues' all repositories
+func (issues IssueList) LoadRepositories() ([]*repo_model.Repository, error) {
+ return issues.loadRepositories(db.DefaultContext)
+}
+
+func (issues IssueList) getPosterIDs() []int64 {
+ posterIDs := make(map[int64]struct{}, len(issues))
+ for _, issue := range issues {
+ if _, ok := posterIDs[issue.PosterID]; !ok {
+ posterIDs[issue.PosterID] = struct{}{}
+ }
+ }
+ return container.KeysInt64(posterIDs)
+}
+
+func (issues IssueList) loadPosters(ctx context.Context) error {
+ if len(issues) == 0 {
+ return nil
+ }
+
+ posterIDs := issues.getPosterIDs()
+ posterMaps := make(map[int64]*user_model.User, len(posterIDs))
+ left := len(posterIDs)
+ for left > 0 {
+ limit := db.DefaultMaxInSize
+ if left < limit {
+ limit = left
+ }
+ err := db.GetEngine(ctx).
+ In("id", posterIDs[:limit]).
+ Find(&posterMaps)
+ if err != nil {
+ return err
+ }
+ left -= limit
+ posterIDs = posterIDs[limit:]
+ }
+
+ for _, issue := range issues {
+ if issue.PosterID <= 0 {
+ continue
+ }
+ var ok bool
+ if issue.Poster, ok = posterMaps[issue.PosterID]; !ok {
+ issue.Poster = user_model.NewGhostUser()
+ }
+ }
+ return nil
+}
+
+func (issues IssueList) getIssueIDs() []int64 {
+ ids := make([]int64, 0, len(issues))
+ for _, issue := range issues {
+ ids = append(ids, issue.ID)
+ }
+ return ids
+}
+
+func (issues IssueList) loadLabels(ctx context.Context) error {
+ if len(issues) == 0 {
+ return nil
+ }
+
+ type LabelIssue struct {
+ Label *Label `xorm:"extends"`
+ IssueLabel *IssueLabel `xorm:"extends"`
+ }
+
+ issueLabels := make(map[int64][]*Label, len(issues)*3)
+ issueIDs := issues.getIssueIDs()
+ left := len(issueIDs)
+ for left > 0 {
+ limit := db.DefaultMaxInSize
+ if left < limit {
+ limit = left
+ }
+ rows, err := db.GetEngine(ctx).Table("label").
+ Join("LEFT", "issue_label", "issue_label.label_id = label.id").
+ In("issue_label.issue_id", issueIDs[:limit]).
+ Asc("label.name").
+ Rows(new(LabelIssue))
+ if err != nil {
+ return err
+ }
+
+ for rows.Next() {
+ var labelIssue LabelIssue
+ err = rows.Scan(&labelIssue)
+ if err != nil {
+ if err1 := rows.Close(); err1 != nil {
+ return fmt.Errorf("IssueList.loadLabels: Close: %v", err1)
+ }
+ return err
+ }
+ issueLabels[labelIssue.IssueLabel.IssueID] = append(issueLabels[labelIssue.IssueLabel.IssueID], labelIssue.Label)
+ }
+ // When there are no rows left and we try to close it.
+ // Since that is not relevant for us, we can safely ignore it.
+ if err1 := rows.Close(); err1 != nil {
+ return fmt.Errorf("IssueList.loadLabels: Close: %v", err1)
+ }
+ left -= limit
+ issueIDs = issueIDs[limit:]
+ }
+
+ for _, issue := range issues {
+ issue.Labels = issueLabels[issue.ID]
+ }
+ return nil
+}
+
+func (issues IssueList) getMilestoneIDs() []int64 {
+ ids := make(map[int64]struct{}, len(issues))
+ for _, issue := range issues {
+ if _, ok := ids[issue.MilestoneID]; !ok {
+ ids[issue.MilestoneID] = struct{}{}
+ }
+ }
+ return container.KeysInt64(ids)
+}
+
+func (issues IssueList) loadMilestones(ctx context.Context) error {
+ milestoneIDs := issues.getMilestoneIDs()
+ if len(milestoneIDs) == 0 {
+ return nil
+ }
+
+ milestoneMaps := make(map[int64]*Milestone, len(milestoneIDs))
+ left := len(milestoneIDs)
+ for left > 0 {
+ limit := db.DefaultMaxInSize
+ if left < limit {
+ limit = left
+ }
+ err := db.GetEngine(ctx).
+ In("id", milestoneIDs[:limit]).
+ Find(&milestoneMaps)
+ if err != nil {
+ return err
+ }
+ left -= limit
+ milestoneIDs = milestoneIDs[limit:]
+ }
+
+ for _, issue := range issues {
+ issue.Milestone = milestoneMaps[issue.MilestoneID]
+ }
+ return nil
+}
+
+func (issues IssueList) loadAssignees(ctx context.Context) error {
+ if len(issues) == 0 {
+ return nil
+ }
+
+ type AssigneeIssue struct {
+ IssueAssignee *IssueAssignees `xorm:"extends"`
+ Assignee *user_model.User `xorm:"extends"`
+ }
+
+ assignees := make(map[int64][]*user_model.User, len(issues))
+ issueIDs := issues.getIssueIDs()
+ left := len(issueIDs)
+ for left > 0 {
+ limit := db.DefaultMaxInSize
+ if left < limit {
+ limit = left
+ }
+ rows, err := db.GetEngine(ctx).Table("issue_assignees").
+ Join("INNER", "`user`", "`user`.id = `issue_assignees`.assignee_id").
+ In("`issue_assignees`.issue_id", issueIDs[:limit]).
+ Rows(new(AssigneeIssue))
+ if err != nil {
+ return err
+ }
+
+ for rows.Next() {
+ var assigneeIssue AssigneeIssue
+ err = rows.Scan(&assigneeIssue)
+ if err != nil {
+ if err1 := rows.Close(); err1 != nil {
+ return fmt.Errorf("IssueList.loadAssignees: Close: %v", err1)
+ }
+ return err
+ }
+
+ assignees[assigneeIssue.IssueAssignee.IssueID] = append(assignees[assigneeIssue.IssueAssignee.IssueID], assigneeIssue.Assignee)
+ }
+ if err1 := rows.Close(); err1 != nil {
+ return fmt.Errorf("IssueList.loadAssignees: Close: %v", err1)
+ }
+ left -= limit
+ issueIDs = issueIDs[limit:]
+ }
+
+ for _, issue := range issues {
+ issue.Assignees = assignees[issue.ID]
+ }
+ return nil
+}
+
+func (issues IssueList) getPullIssueIDs() []int64 {
+ ids := make([]int64, 0, len(issues))
+ for _, issue := range issues {
+ if issue.IsPull && issue.PullRequest == nil {
+ ids = append(ids, issue.ID)
+ }
+ }
+ return ids
+}
+
+func (issues IssueList) loadPullRequests(ctx context.Context) error {
+ issuesIDs := issues.getPullIssueIDs()
+ if len(issuesIDs) == 0 {
+ return nil
+ }
+
+ pullRequestMaps := make(map[int64]*PullRequest, len(issuesIDs))
+ left := len(issuesIDs)
+ for left > 0 {
+ limit := db.DefaultMaxInSize
+ if left < limit {
+ limit = left
+ }
+ rows, err := db.GetEngine(ctx).
+ In("issue_id", issuesIDs[:limit]).
+ Rows(new(PullRequest))
+ if err != nil {
+ return err
+ }
+
+ for rows.Next() {
+ var pr PullRequest
+ err = rows.Scan(&pr)
+ if err != nil {
+ if err1 := rows.Close(); err1 != nil {
+ return fmt.Errorf("IssueList.loadPullRequests: Close: %v", err1)
+ }
+ return err
+ }
+ pullRequestMaps[pr.IssueID] = &pr
+ }
+ if err1 := rows.Close(); err1 != nil {
+ return fmt.Errorf("IssueList.loadPullRequests: Close: %v", err1)
+ }
+ left -= limit
+ issuesIDs = issuesIDs[limit:]
+ }
+
+ for _, issue := range issues {
+ issue.PullRequest = pullRequestMaps[issue.ID]
+ }
+ return nil
+}
+
+func (issues IssueList) loadAttachments(ctx context.Context) (err error) {
+ if len(issues) == 0 {
+ return nil
+ }
+
+ attachments := make(map[int64][]*repo_model.Attachment, len(issues))
+ issuesIDs := issues.getIssueIDs()
+ left := len(issuesIDs)
+ for left > 0 {
+ limit := db.DefaultMaxInSize
+ if left < limit {
+ limit = left
+ }
+ rows, err := db.GetEngine(ctx).Table("attachment").
+ Join("INNER", "issue", "issue.id = attachment.issue_id").
+ In("issue.id", issuesIDs[:limit]).
+ Rows(new(repo_model.Attachment))
+ if err != nil {
+ return err
+ }
+
+ for rows.Next() {
+ var attachment repo_model.Attachment
+ err = rows.Scan(&attachment)
+ if err != nil {
+ if err1 := rows.Close(); err1 != nil {
+ return fmt.Errorf("IssueList.loadAttachments: Close: %v", err1)
+ }
+ return err
+ }
+ attachments[attachment.IssueID] = append(attachments[attachment.IssueID], &attachment)
+ }
+ if err1 := rows.Close(); err1 != nil {
+ return fmt.Errorf("IssueList.loadAttachments: Close: %v", err1)
+ }
+ left -= limit
+ issuesIDs = issuesIDs[limit:]
+ }
+
+ for _, issue := range issues {
+ issue.Attachments = attachments[issue.ID]
+ }
+ return nil
+}
+
+func (issues IssueList) loadComments(ctx context.Context, cond builder.Cond) (err error) {
+ if len(issues) == 0 {
+ return nil
+ }
+
+ comments := make(map[int64][]*Comment, len(issues))
+ issuesIDs := issues.getIssueIDs()
+ left := len(issuesIDs)
+ for left > 0 {
+ limit := db.DefaultMaxInSize
+ if left < limit {
+ limit = left
+ }
+ rows, err := db.GetEngine(ctx).Table("comment").
+ Join("INNER", "issue", "issue.id = comment.issue_id").
+ In("issue.id", issuesIDs[:limit]).
+ Where(cond).
+ Rows(new(Comment))
+ if err != nil {
+ return err
+ }
+
+ for rows.Next() {
+ var comment Comment
+ err = rows.Scan(&comment)
+ if err != nil {
+ if err1 := rows.Close(); err1 != nil {
+ return fmt.Errorf("IssueList.loadComments: Close: %v", err1)
+ }
+ return err
+ }
+ comments[comment.IssueID] = append(comments[comment.IssueID], &comment)
+ }
+ if err1 := rows.Close(); err1 != nil {
+ return fmt.Errorf("IssueList.loadComments: Close: %v", err1)
+ }
+ left -= limit
+ issuesIDs = issuesIDs[limit:]
+ }
+
+ for _, issue := range issues {
+ issue.Comments = comments[issue.ID]
+ }
+ return nil
+}
+
+func (issues IssueList) loadTotalTrackedTimes(ctx context.Context) (err error) {
+ type totalTimesByIssue struct {
+ IssueID int64
+ Time int64
+ }
+ if len(issues) == 0 {
+ return nil
+ }
+ trackedTimes := make(map[int64]int64, len(issues))
+
+ ids := make([]int64, 0, len(issues))
+ for _, issue := range issues {
+ if issue.Repo.IsTimetrackerEnabled() {
+ ids = append(ids, issue.ID)
+ }
+ }
+
+ left := len(ids)
+ for left > 0 {
+ limit := db.DefaultMaxInSize
+ if left < limit {
+ limit = left
+ }
+
+ // select issue_id, sum(time) from tracked_time where issue_id in (<issue ids in current page>) group by issue_id
+ rows, err := db.GetEngine(ctx).Table("tracked_time").
+ Where("deleted = ?", false).
+ Select("issue_id, sum(time) as time").
+ In("issue_id", ids[:limit]).
+ GroupBy("issue_id").
+ Rows(new(totalTimesByIssue))
+ if err != nil {
+ return err
+ }
+
+ for rows.Next() {
+ var totalTime totalTimesByIssue
+ err = rows.Scan(&totalTime)
+ if err != nil {
+ if err1 := rows.Close(); err1 != nil {
+ return fmt.Errorf("IssueList.loadTotalTrackedTimes: Close: %v", err1)
+ }
+ return err
+ }
+ trackedTimes[totalTime.IssueID] = totalTime.Time
+ }
+ if err1 := rows.Close(); err1 != nil {
+ return fmt.Errorf("IssueList.loadTotalTrackedTimes: Close: %v", err1)
+ }
+ left -= limit
+ ids = ids[limit:]
+ }
+
+ for _, issue := range issues {
+ issue.TotalTrackedTime = trackedTimes[issue.ID]
+ }
+ return nil
+}
+
+// loadAttributes loads all attributes, expect for attachments and comments
+func (issues IssueList) loadAttributes(ctx context.Context) error {
+ if _, err := issues.loadRepositories(ctx); err != nil {
+ return fmt.Errorf("issue.loadAttributes: loadRepositories: %v", err)
+ }
+
+ if err := issues.loadPosters(ctx); err != nil {
+ return fmt.Errorf("issue.loadAttributes: loadPosters: %v", err)
+ }
+
+ if err := issues.loadLabels(ctx); err != nil {
+ return fmt.Errorf("issue.loadAttributes: loadLabels: %v", err)
+ }
+
+ if err := issues.loadMilestones(ctx); err != nil {
+ return fmt.Errorf("issue.loadAttributes: loadMilestones: %v", err)
+ }
+
+ if err := issues.loadAssignees(ctx); err != nil {
+ return fmt.Errorf("issue.loadAttributes: loadAssignees: %v", err)
+ }
+
+ if err := issues.loadPullRequests(ctx); err != nil {
+ return fmt.Errorf("issue.loadAttributes: loadPullRequests: %v", err)
+ }
+
+ if err := issues.loadTotalTrackedTimes(ctx); err != nil {
+ return fmt.Errorf("issue.loadAttributes: loadTotalTrackedTimes: %v", err)
+ }
+
+ return nil
+}
+
+// LoadAttributes loads attributes of the issues, except for attachments and
+// comments
+func (issues IssueList) LoadAttributes() error {
+ return issues.loadAttributes(db.DefaultContext)
+}
+
+// LoadAttachments loads attachments
+func (issues IssueList) LoadAttachments() error {
+ return issues.loadAttachments(db.DefaultContext)
+}
+
+// LoadComments loads comments
+func (issues IssueList) LoadComments() error {
+ return issues.loadComments(db.DefaultContext, builder.NewCond())
+}
+
+// LoadDiscussComments loads discuss comments
+func (issues IssueList) LoadDiscussComments() error {
+ return issues.loadComments(db.DefaultContext, builder.Eq{"comment.type": CommentTypeComment})
+}
+
+// LoadPullRequests loads pull requests
+func (issues IssueList) LoadPullRequests() error {
+ return issues.loadPullRequests(db.DefaultContext)
+}
+
+// GetApprovalCounts returns a map of issue ID to slice of approval counts
+// FIXME: only returns official counts due to double counting of non-official approvals
+func (issues IssueList) GetApprovalCounts(ctx context.Context) (map[int64][]*ReviewCount, error) {
+ rCounts := make([]*ReviewCount, 0, 2*len(issues))
+ ids := make([]int64, len(issues))
+ for i, issue := range issues {
+ ids[i] = issue.ID
+ }
+ sess := db.GetEngine(ctx).In("issue_id", ids)
+ err := sess.Select("issue_id, type, count(id) as `count`").
+ Where("official = ? AND dismissed = ?", true, false).
+ GroupBy("issue_id, type").
+ OrderBy("issue_id").
+ Table("review").
+ Find(&rCounts)
+ if err != nil {
+ return nil, err
+ }
+
+ approvalCountMap := make(map[int64][]*ReviewCount, len(issues))
+
+ for _, c := range rCounts {
+ approvalCountMap[c.IssueID] = append(approvalCountMap[c.IssueID], c)
+ }
+
+ return approvalCountMap, nil
+}