123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380 |
- // 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{}
-
- countSession := func(opts *IssuesOptions, issueIDs []int64) *xorm.Session {
- sess := db.GetEngine(db.DefaultContext).
- Join("INNER", "repository", "`issue`.repo_id = `repository`.id")
- 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
- }
-
- var err error
- stats.OpenCount, err = countSession(opts, issueIDs).
- And("issue.is_closed = ?", false).
- Count(new(Issue))
- if err != nil {
- return stats, err
- }
- stats.ClosedCount, err = countSession(opts, issueIDs).
- And("issue.is_closed = ?", true).
- Count(new(Issue))
- return stats, err
- }
-
- // 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()
- }
|