mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-29 10:57:44 +09:00 
			
		
		
		
	move inner **countSession** of **getIssueStatsChunk** into it's own function for reuse --- *Sponsored by Kithara Software GmbH*
		
			
				
	
	
		
			382 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			382 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // Copyright 2023 The Gitea Authors. All rights reserved.
 | |
| // SPDX-License-Identifier: MIT
 | |
| 
 | |
| package issues
 | |
| 
 | |
| import (
 | |
| 	"context"
 | |
| 	"errors"
 | |
| 	"fmt"
 | |
| 
 | |
| 	"code.gitea.io/gitea/models/db"
 | |
| 	"code.gitea.io/gitea/modules/util"
 | |
| 
 | |
| 	"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) (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)
 | |
| 
 | |
| 	return sess.Count()
 | |
| }
 | |
| 
 | |
| // GetIssueStats returns issue statistic information by given conditions.
 | |
| func GetIssueStats(opts *IssuesOptions) (*IssueStats, error) {
 | |
| 	if len(opts.IssueIDs) <= MaxQueryParameters {
 | |
| 		return getIssueStatsChunk(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(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.OpenCount += stats.MentionCount
 | |
| 		accum.ReviewRequestedCount += stats.ReviewRequestedCount
 | |
| 		accum.ReviewedCount += stats.ReviewedCount
 | |
| 		i = chunk
 | |
| 	}
 | |
| 	return accum, nil
 | |
| }
 | |
| 
 | |
| func getIssueStatsChunk(opts *IssuesOptions, issueIDs []int64) (*IssueStats, error) {
 | |
| 	stats := &IssueStats{}
 | |
| 
 | |
| 	sess := db.GetEngine(db.DefaultContext).
 | |
| 		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)
 | |
| 	}
 | |
| 
 | |
| 	switch opts.IsPull {
 | |
| 	case util.OptionalBoolTrue:
 | |
| 		sess.And("issue.is_pull=?", true)
 | |
| 	case util.OptionalBoolFalse:
 | |
| 		sess.And("issue.is_pull=?", false)
 | |
| 	}
 | |
| 
 | |
| 	return sess
 | |
| }
 | |
| 
 | |
| // GetUserIssueStats returns issue statistic information for dashboard by given conditions.
 | |
| func GetUserIssueStats(filterMode int, opts IssuesOptions) (*IssueStats, error) {
 | |
| 	if opts.User == nil {
 | |
| 		return nil, errors.New("issue stats without user")
 | |
| 	}
 | |
| 	if opts.IsPull.IsNone() {
 | |
| 		return nil, errors.New("unaccepted ispull option")
 | |
| 	}
 | |
| 
 | |
| 	var err error
 | |
| 	stats := &IssueStats{}
 | |
| 
 | |
| 	cond := builder.NewCond()
 | |
| 
 | |
| 	cond = cond.And(builder.Eq{"issue.is_pull": opts.IsPull.IsTrue()})
 | |
| 
 | |
| 	if len(opts.RepoIDs) > 0 {
 | |
| 		cond = cond.And(builder.In("issue.repo_id", opts.RepoIDs))
 | |
| 	}
 | |
| 	if len(opts.IssueIDs) > 0 {
 | |
| 		cond = cond.And(builder.In("issue.id", opts.IssueIDs))
 | |
| 	}
 | |
| 	if opts.RepoCond != nil {
 | |
| 		cond = cond.And(opts.RepoCond)
 | |
| 	}
 | |
| 
 | |
| 	if opts.User != nil {
 | |
| 		cond = cond.And(issuePullAccessibleRepoCond("issue.repo_id", opts.User.ID, opts.Org, opts.Team, opts.IsPull.IsTrue()))
 | |
| 	}
 | |
| 
 | |
| 	sess := func(cond builder.Cond) *xorm.Session {
 | |
| 		s := db.GetEngine(db.DefaultContext).
 | |
| 			Join("INNER", "repository", "`issue`.repo_id = `repository`.id").
 | |
| 			Where(cond)
 | |
| 		if len(opts.LabelIDs) > 0 {
 | |
| 			s.Join("INNER", "issue_label", "issue_label.issue_id = issue.id").
 | |
| 				In("issue_label.label_id", opts.LabelIDs)
 | |
| 		}
 | |
| 
 | |
| 		if opts.IsArchived != util.OptionalBoolNone {
 | |
| 			s.And(builder.Eq{"repository.is_archived": opts.IsArchived.IsTrue()})
 | |
| 		}
 | |
| 		return s
 | |
| 	}
 | |
| 
 | |
| 	switch filterMode {
 | |
| 	case FilterModeAll, FilterModeYourRepositories:
 | |
| 		stats.OpenCount, err = sess(cond).
 | |
| 			And("issue.is_closed = ?", false).
 | |
| 			Count(new(Issue))
 | |
| 		if err != nil {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 		stats.ClosedCount, err = sess(cond).
 | |
| 			And("issue.is_closed = ?", true).
 | |
| 			Count(new(Issue))
 | |
| 		if err != nil {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 	case FilterModeAssign:
 | |
| 		stats.OpenCount, err = applyAssigneeCondition(sess(cond), opts.User.ID).
 | |
| 			And("issue.is_closed = ?", false).
 | |
| 			Count(new(Issue))
 | |
| 		if err != nil {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 		stats.ClosedCount, err = applyAssigneeCondition(sess(cond), opts.User.ID).
 | |
| 			And("issue.is_closed = ?", true).
 | |
| 			Count(new(Issue))
 | |
| 		if err != nil {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 	case FilterModeCreate:
 | |
| 		stats.OpenCount, err = applyPosterCondition(sess(cond), opts.User.ID).
 | |
| 			And("issue.is_closed = ?", false).
 | |
| 			Count(new(Issue))
 | |
| 		if err != nil {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 		stats.ClosedCount, err = applyPosterCondition(sess(cond), opts.User.ID).
 | |
| 			And("issue.is_closed = ?", true).
 | |
| 			Count(new(Issue))
 | |
| 		if err != nil {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 	case FilterModeMention:
 | |
| 		stats.OpenCount, err = applyMentionedCondition(sess(cond), opts.User.ID).
 | |
| 			And("issue.is_closed = ?", false).
 | |
| 			Count(new(Issue))
 | |
| 		if err != nil {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 		stats.ClosedCount, err = applyMentionedCondition(sess(cond), opts.User.ID).
 | |
| 			And("issue.is_closed = ?", true).
 | |
| 			Count(new(Issue))
 | |
| 		if err != nil {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 	case FilterModeReviewRequested:
 | |
| 		stats.OpenCount, err = applyReviewRequestedCondition(sess(cond), opts.User.ID).
 | |
| 			And("issue.is_closed = ?", false).
 | |
| 			Count(new(Issue))
 | |
| 		if err != nil {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 		stats.ClosedCount, err = applyReviewRequestedCondition(sess(cond), opts.User.ID).
 | |
| 			And("issue.is_closed = ?", true).
 | |
| 			Count(new(Issue))
 | |
| 		if err != nil {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 	case FilterModeReviewed:
 | |
| 		stats.OpenCount, err = applyReviewedCondition(sess(cond), opts.User.ID).
 | |
| 			And("issue.is_closed = ?", false).
 | |
| 			Count(new(Issue))
 | |
| 		if err != nil {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 		stats.ClosedCount, err = applyReviewedCondition(sess(cond), opts.User.ID).
 | |
| 			And("issue.is_closed = ?", true).
 | |
| 			Count(new(Issue))
 | |
| 		if err != nil {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	cond = cond.And(builder.Eq{"issue.is_closed": opts.IsClosed.IsTrue()})
 | |
| 	stats.AssignCount, err = applyAssigneeCondition(sess(cond), opts.User.ID).Count(new(Issue))
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	stats.CreateCount, err = applyPosterCondition(sess(cond), opts.User.ID).Count(new(Issue))
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	stats.MentionCount, err = applyMentionedCondition(sess(cond), opts.User.ID).Count(new(Issue))
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	stats.YourRepositoriesCount, err = sess(cond).Count(new(Issue))
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	stats.ReviewRequestedCount, err = applyReviewRequestedCondition(sess(cond), opts.User.ID).Count(new(Issue))
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	stats.ReviewedCount, err = applyReviewedCondition(sess(cond), opts.User.ID).Count(new(Issue))
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	return stats, nil
 | |
| }
 | |
| 
 | |
| // GetRepoIssueStats returns number of open and closed repository issues by given filter mode.
 | |
| func GetRepoIssueStats(repoID, uid int64, filterMode int, isPull bool) (numOpen, numClosed int64) {
 | |
| 	countSession := func(isClosed, isPull bool, repoID int64) *xorm.Session {
 | |
| 		sess := db.GetEngine(db.DefaultContext).
 | |
| 			Where("is_closed = ?", isClosed).
 | |
| 			And("is_pull = ?", isPull).
 | |
| 			And("repo_id = ?", repoID)
 | |
| 
 | |
| 		return sess
 | |
| 	}
 | |
| 
 | |
| 	openCountSession := countSession(false, isPull, repoID)
 | |
| 	closedCountSession := countSession(true, isPull, repoID)
 | |
| 
 | |
| 	switch filterMode {
 | |
| 	case FilterModeAssign:
 | |
| 		applyAssigneeCondition(openCountSession, uid)
 | |
| 		applyAssigneeCondition(closedCountSession, uid)
 | |
| 	case FilterModeCreate:
 | |
| 		applyPosterCondition(openCountSession, uid)
 | |
| 		applyPosterCondition(closedCountSession, uid)
 | |
| 	}
 | |
| 
 | |
| 	openResult, _ := openCountSession.Count(new(Issue))
 | |
| 	closedResult, _ := closedCountSession.Count(new(Issue))
 | |
| 
 | |
| 	return openResult, closedResult
 | |
| }
 | |
| 
 | |
| // 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()
 | |
| }
 |