summaryrefslogtreecommitdiffstats
path: root/models/issues/issue_stats.go
diff options
context:
space:
mode:
Diffstat (limited to 'models/issues/issue_stats.go')
-rw-r--r--models/issues/issue_stats.go191
1 files changed, 191 insertions, 0 deletions
diff --git a/models/issues/issue_stats.go b/models/issues/issue_stats.go
new file mode 100644
index 0000000..dc634cf
--- /dev/null
+++ b/models/issues/issue_stats.go
@@ -0,0 +1,191 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package issues
+
+import (
+ "context"
+ "fmt"
+
+ "code.gitea.io/gitea/models/db"
+
+ "xorm.io/builder"
+ "xorm.io/xorm"
+)
+
+// IssueStats represents issue statistic information.
+type IssueStats struct {
+ OpenCount, ClosedCount int64
+ YourRepositoriesCount int64
+ AssignCount int64
+ CreateCount int64
+ MentionCount int64
+ ReviewRequestedCount int64
+ ReviewedCount int64
+}
+
+// Filter modes.
+const (
+ FilterModeAll = iota
+ FilterModeAssign
+ FilterModeCreate
+ FilterModeMention
+ FilterModeReviewRequested
+ FilterModeReviewed
+ FilterModeYourRepositories
+)
+
+const (
+ // MaxQueryParameters represents the max query parameters
+ // When queries are broken down in parts because of the number
+ // of parameters, attempt to break by this amount
+ MaxQueryParameters = 300
+)
+
+// CountIssuesByRepo map from repoID to number of issues matching the options
+func CountIssuesByRepo(ctx context.Context, opts *IssuesOptions) (map[int64]int64, error) {
+ sess := db.GetEngine(ctx).
+ Join("INNER", "repository", "`issue`.repo_id = `repository`.id")
+
+ applyConditions(sess, opts)
+
+ countsSlice := make([]*struct {
+ RepoID int64
+ Count int64
+ }, 0, 10)
+ if err := sess.GroupBy("issue.repo_id").
+ Select("issue.repo_id AS repo_id, COUNT(*) AS count").
+ Table("issue").
+ Find(&countsSlice); err != nil {
+ return nil, fmt.Errorf("unable to CountIssuesByRepo: %w", err)
+ }
+
+ countMap := make(map[int64]int64, len(countsSlice))
+ for _, c := range countsSlice {
+ countMap[c.RepoID] = c.Count
+ }
+ return countMap, nil
+}
+
+// CountIssues number return of issues by given conditions.
+func CountIssues(ctx context.Context, opts *IssuesOptions, otherConds ...builder.Cond) (int64, error) {
+ sess := db.GetEngine(ctx).
+ Select("COUNT(issue.id) AS count").
+ Table("issue").
+ Join("INNER", "repository", "`issue`.repo_id = `repository`.id")
+ applyConditions(sess, opts)
+
+ for _, cond := range otherConds {
+ sess.And(cond)
+ }
+
+ return sess.Count()
+}
+
+// GetIssueStats returns issue statistic information by given conditions.
+func GetIssueStats(ctx context.Context, opts *IssuesOptions) (*IssueStats, error) {
+ if len(opts.IssueIDs) <= MaxQueryParameters {
+ return getIssueStatsChunk(ctx, opts, opts.IssueIDs)
+ }
+
+ // If too long a list of IDs is provided, we get the statistics in
+ // smaller chunks and get accumulates. Note: this could potentially
+ // get us invalid results. The alternative is to insert the list of
+ // ids in a temporary table and join from them.
+ accum := &IssueStats{}
+ for i := 0; i < len(opts.IssueIDs); {
+ chunk := i + MaxQueryParameters
+ if chunk > len(opts.IssueIDs) {
+ chunk = len(opts.IssueIDs)
+ }
+ stats, err := getIssueStatsChunk(ctx, opts, opts.IssueIDs[i:chunk])
+ if err != nil {
+ return nil, err
+ }
+ accum.OpenCount += stats.OpenCount
+ accum.ClosedCount += stats.ClosedCount
+ accum.YourRepositoriesCount += stats.YourRepositoriesCount
+ accum.AssignCount += stats.AssignCount
+ accum.CreateCount += stats.CreateCount
+ accum.MentionCount += stats.MentionCount
+ accum.ReviewRequestedCount += stats.ReviewRequestedCount
+ accum.ReviewedCount += stats.ReviewedCount
+ i = chunk
+ }
+ return accum, nil
+}
+
+func getIssueStatsChunk(ctx context.Context, opts *IssuesOptions, issueIDs []int64) (*IssueStats, error) {
+ stats := &IssueStats{}
+
+ sess := db.GetEngine(ctx).
+ Join("INNER", "repository", "`issue`.repo_id = `repository`.id")
+
+ var err error
+ stats.OpenCount, err = applyIssuesOptions(sess, opts, issueIDs).
+ And("issue.is_closed = ?", false).
+ Count(new(Issue))
+ if err != nil {
+ return stats, err
+ }
+ stats.ClosedCount, err = applyIssuesOptions(sess, opts, issueIDs).
+ And("issue.is_closed = ?", true).
+ Count(new(Issue))
+ return stats, err
+}
+
+func applyIssuesOptions(sess *xorm.Session, opts *IssuesOptions, issueIDs []int64) *xorm.Session {
+ if len(opts.RepoIDs) > 1 {
+ sess.In("issue.repo_id", opts.RepoIDs)
+ } else if len(opts.RepoIDs) == 1 {
+ sess.And("issue.repo_id = ?", opts.RepoIDs[0])
+ }
+
+ if len(issueIDs) > 0 {
+ sess.In("issue.id", issueIDs)
+ }
+
+ applyLabelsCondition(sess, opts)
+
+ applyMilestoneCondition(sess, opts)
+
+ applyProjectCondition(sess, opts)
+
+ if opts.AssigneeID > 0 {
+ applyAssigneeCondition(sess, opts.AssigneeID)
+ } else if opts.AssigneeID == db.NoConditionID {
+ sess.Where("issue.id NOT IN (SELECT issue_id FROM issue_assignees)")
+ }
+
+ if opts.PosterID > 0 {
+ applyPosterCondition(sess, opts.PosterID)
+ }
+
+ if opts.MentionedID > 0 {
+ applyMentionedCondition(sess, opts.MentionedID)
+ }
+
+ if opts.ReviewRequestedID > 0 {
+ applyReviewRequestedCondition(sess, opts.ReviewRequestedID)
+ }
+
+ if opts.ReviewedID > 0 {
+ applyReviewedCondition(sess, opts.ReviewedID)
+ }
+
+ if opts.IsPull.Has() {
+ sess.And("issue.is_pull=?", opts.IsPull.Value())
+ }
+
+ return sess
+}
+
+// CountOrphanedIssues count issues without a repo
+func CountOrphanedIssues(ctx context.Context) (int64, error) {
+ return db.GetEngine(ctx).
+ Table("issue").
+ Join("LEFT", "repository", "issue.repo_id=repository.id").
+ Where(builder.IsNull{"repository.id"}).
+ Select("COUNT(`issue`.`id`)").
+ Count()
+}