191 lines
		
	
	
	
		
			5 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			191 lines
		
	
	
	
		
			5 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // 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()
 | |
| }
 | 
