2023-05-19 17:17:48 +03:00
// 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 { }
2023-08-23 04:29:49 +03:00
sess := db . GetEngine ( db . DefaultContext ) .
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
}
2023-08-23 04:29:49 +03:00
switch opts . IsPull {
case util . OptionalBoolTrue :
sess . And ( "issue.is_pull=?" , true )
case util . OptionalBoolFalse :
sess . And ( "issue.is_pull=?" , false )
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
}
// 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 ( )
}