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  }