code.gitea.io/gitea@v1.22.3/models/issues/issue_stats.go (about) 1 // Copyright 2023 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 12 "xorm.io/builder" 13 "xorm.io/xorm" 14 ) 15 16 // IssueStats represents issue statistic information. 17 type IssueStats struct { 18 OpenCount, ClosedCount int64 19 YourRepositoriesCount int64 20 AssignCount int64 21 CreateCount int64 22 MentionCount int64 23 ReviewRequestedCount int64 24 ReviewedCount int64 25 } 26 27 // Filter modes. 28 const ( 29 FilterModeAll = iota 30 FilterModeAssign 31 FilterModeCreate 32 FilterModeMention 33 FilterModeReviewRequested 34 FilterModeReviewed 35 FilterModeYourRepositories 36 ) 37 38 const ( 39 // MaxQueryParameters represents the max query parameters 40 // When queries are broken down in parts because of the number 41 // of parameters, attempt to break by this amount 42 MaxQueryParameters = 300 43 ) 44 45 // CountIssuesByRepo map from repoID to number of issues matching the options 46 func CountIssuesByRepo(ctx context.Context, opts *IssuesOptions) (map[int64]int64, error) { 47 sess := db.GetEngine(ctx). 48 Join("INNER", "repository", "`issue`.repo_id = `repository`.id") 49 50 applyConditions(sess, opts) 51 52 countsSlice := make([]*struct { 53 RepoID int64 54 Count int64 55 }, 0, 10) 56 if err := sess.GroupBy("issue.repo_id"). 57 Select("issue.repo_id AS repo_id, COUNT(*) AS count"). 58 Table("issue"). 59 Find(&countsSlice); err != nil { 60 return nil, fmt.Errorf("unable to CountIssuesByRepo: %w", err) 61 } 62 63 countMap := make(map[int64]int64, len(countsSlice)) 64 for _, c := range countsSlice { 65 countMap[c.RepoID] = c.Count 66 } 67 return countMap, nil 68 } 69 70 // CountIssues number return of issues by given conditions. 71 func CountIssues(ctx context.Context, opts *IssuesOptions, otherConds ...builder.Cond) (int64, error) { 72 sess := db.GetEngine(ctx). 73 Select("COUNT(issue.id) AS count"). 74 Table("issue"). 75 Join("INNER", "repository", "`issue`.repo_id = `repository`.id") 76 applyConditions(sess, opts) 77 78 for _, cond := range otherConds { 79 sess.And(cond) 80 } 81 82 return sess.Count() 83 } 84 85 // GetIssueStats returns issue statistic information by given conditions. 86 func GetIssueStats(ctx context.Context, opts *IssuesOptions) (*IssueStats, error) { 87 if len(opts.IssueIDs) <= MaxQueryParameters { 88 return getIssueStatsChunk(ctx, opts, opts.IssueIDs) 89 } 90 91 // If too long a list of IDs is provided, we get the statistics in 92 // smaller chunks and get accumulates. Note: this could potentially 93 // get us invalid results. The alternative is to insert the list of 94 // ids in a temporary table and join from them. 95 accum := &IssueStats{} 96 for i := 0; i < len(opts.IssueIDs); { 97 chunk := i + MaxQueryParameters 98 if chunk > len(opts.IssueIDs) { 99 chunk = len(opts.IssueIDs) 100 } 101 stats, err := getIssueStatsChunk(ctx, opts, opts.IssueIDs[i:chunk]) 102 if err != nil { 103 return nil, err 104 } 105 accum.OpenCount += stats.OpenCount 106 accum.ClosedCount += stats.ClosedCount 107 accum.YourRepositoriesCount += stats.YourRepositoriesCount 108 accum.AssignCount += stats.AssignCount 109 accum.CreateCount += stats.CreateCount 110 accum.OpenCount += stats.MentionCount 111 accum.ReviewRequestedCount += stats.ReviewRequestedCount 112 accum.ReviewedCount += stats.ReviewedCount 113 i = chunk 114 } 115 return accum, nil 116 } 117 118 func getIssueStatsChunk(ctx context.Context, opts *IssuesOptions, issueIDs []int64) (*IssueStats, error) { 119 stats := &IssueStats{} 120 121 sess := db.GetEngine(ctx). 122 Join("INNER", "repository", "`issue`.repo_id = `repository`.id") 123 124 var err error 125 stats.OpenCount, err = applyIssuesOptions(sess, opts, issueIDs). 126 And("issue.is_closed = ?", false). 127 Count(new(Issue)) 128 if err != nil { 129 return stats, err 130 } 131 stats.ClosedCount, err = applyIssuesOptions(sess, opts, issueIDs). 132 And("issue.is_closed = ?", true). 133 Count(new(Issue)) 134 return stats, err 135 } 136 137 func applyIssuesOptions(sess *xorm.Session, opts *IssuesOptions, issueIDs []int64) *xorm.Session { 138 if len(opts.RepoIDs) > 1 { 139 sess.In("issue.repo_id", opts.RepoIDs) 140 } else if len(opts.RepoIDs) == 1 { 141 sess.And("issue.repo_id = ?", opts.RepoIDs[0]) 142 } 143 144 if len(issueIDs) > 0 { 145 sess.In("issue.id", issueIDs) 146 } 147 148 applyLabelsCondition(sess, opts) 149 150 applyMilestoneCondition(sess, opts) 151 152 applyProjectCondition(sess, opts) 153 154 if opts.AssigneeID > 0 { 155 applyAssigneeCondition(sess, opts.AssigneeID) 156 } else if opts.AssigneeID == db.NoConditionID { 157 sess.Where("issue.id NOT IN (SELECT issue_id FROM issue_assignees)") 158 } 159 160 if opts.PosterID > 0 { 161 applyPosterCondition(sess, opts.PosterID) 162 } 163 164 if opts.MentionedID > 0 { 165 applyMentionedCondition(sess, opts.MentionedID) 166 } 167 168 if opts.ReviewRequestedID > 0 { 169 applyReviewRequestedCondition(sess, opts.ReviewRequestedID) 170 } 171 172 if opts.ReviewedID > 0 { 173 applyReviewedCondition(sess, opts.ReviewedID) 174 } 175 176 if opts.IsPull.Has() { 177 sess.And("issue.is_pull=?", opts.IsPull.Value()) 178 } 179 180 return sess 181 } 182 183 // CountOrphanedIssues count issues without a repo 184 func CountOrphanedIssues(ctx context.Context) (int64, error) { 185 return db.GetEngine(ctx). 186 Table("issue"). 187 Join("LEFT", "repository", "issue.repo_id=repository.id"). 188 Where(builder.IsNull{"repository.id"}). 189 Select("COUNT(`issue`.`id`)"). 190 Count() 191 }