2023-05-19 17:17:48 +03:00
// 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.
2024-03-24 21:51:08 +03:00
func CountIssues ( ctx context . Context , opts * IssuesOptions , otherConds ... builder . Cond ) ( int64 , error ) {
2023-05-19 17:17:48 +03:00
sess := db . GetEngine ( ctx ) .
Select ( "COUNT(issue.id) AS count" ) .
Table ( "issue" ) .
Join ( "INNER" , "repository" , "`issue`.repo_id = `repository`.id" )
applyConditions ( sess , opts )
2024-03-24 21:51:08 +03:00
for _ , cond := range otherConds {
sess . And ( cond )
}
2023-05-19 17:17:48 +03:00
return sess . Count ( )
}
// GetIssueStats returns issue statistic information by given conditions.
2023-09-25 16:17:37 +03:00
func GetIssueStats ( ctx context . Context , opts * IssuesOptions ) ( * IssueStats , error ) {
2023-05-19 17:17:48 +03:00
if len ( opts . IssueIDs ) <= MaxQueryParameters {
2023-09-25 16:17:37 +03:00
return getIssueStatsChunk ( ctx , opts , opts . IssueIDs )
2023-05-19 17:17:48 +03:00
}
// 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 )
}
2023-09-25 16:17:37 +03:00
stats , err := getIssueStatsChunk ( ctx , opts , opts . IssueIDs [ i : chunk ] )
2023-05-19 17:17:48 +03:00
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
}
2023-09-25 16:17:37 +03:00
func getIssueStatsChunk ( ctx context . Context , opts * IssuesOptions , issueIDs [ ] int64 ) ( * IssueStats , error ) {
2023-05-19 17:17:48 +03:00
stats := & IssueStats { }
2023-09-25 16:17:37 +03:00
sess := db . GetEngine ( ctx ) .
2023-08-23 04:29:49 +03:00
Join ( "INNER" , "repository" , "`issue`.repo_id = `repository`.id" )
2023-05-19 17:17:48 +03:00
2023-08-23 04:29:49 +03:00
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
}
2023-05-19 17:17:48 +03:00
2023-08-23 04:29:49 +03:00
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 ] )
}
2023-05-19 17:17:48 +03:00
2023-08-23 04:29:49 +03:00
if len ( issueIDs ) > 0 {
sess . In ( "issue.id" , issueIDs )
}
2023-05-19 17:17:48 +03:00
2023-08-23 04:29:49 +03:00
applyLabelsCondition ( sess , opts )
2023-05-19 17:17:48 +03:00
2023-08-23 04:29:49 +03:00
applyMilestoneCondition ( sess , opts )
2023-05-19 17:17:48 +03:00
2023-08-23 04:29:49 +03:00
applyProjectCondition ( sess , opts )
2023-05-19 17:17:48 +03:00
2023-08-23 04:29:49 +03:00
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)" )
}
2023-05-19 17:17:48 +03:00
2023-08-23 04:29:49 +03:00
if opts . PosterID > 0 {
applyPosterCondition ( sess , opts . PosterID )
}
2023-05-19 17:17:48 +03:00
2023-08-23 04:29:49 +03:00
if opts . MentionedID > 0 {
applyMentionedCondition ( sess , opts . MentionedID )
}
2023-05-19 17:17:48 +03:00
2023-08-23 04:29:49 +03:00
if opts . ReviewRequestedID > 0 {
applyReviewRequestedCondition ( sess , opts . ReviewRequestedID )
}
2023-05-19 17:17:48 +03:00
2023-08-23 04:29:49 +03:00
if opts . ReviewedID > 0 {
applyReviewedCondition ( sess , opts . ReviewedID )
2023-05-19 17:17:48 +03:00
}
2024-03-02 18:42:31 +03:00
if opts . IsPull . Has ( ) {
sess . And ( "issue.is_pull=?" , opts . IsPull . Value ( ) )
2023-05-19 17:17:48 +03:00
}
2023-08-23 04:29:49 +03:00
return sess
2023-05-19 17:17:48 +03:00
}
// 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 ( )
}