1fc5e41592
The Issue and PullRequest list has 3 states: - open: This lists all open Issues/PullRequests - closed: This lists all closed Issues/PullRequests - all: This lists all open and closed Issues/PullRequests If you want to get to the all state, you need to click Open while in open state or Closed while in closed state, which is very unintuitive. This PR adss a third button to get to this state. ![grafik](/attachments/4ff59e4c-e318-40f0-80ba-f921ce098919) I'm not sure if the eye icon fits well, but I couldn't find a better one. Tests will be added once #4124 is merged. Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/4125 Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org> Co-authored-by: JakobDev <jakobdev@gmx.de> Co-committed-by: JakobDev <jakobdev@gmx.de>
198 lines
5.2 KiB
Go
198 lines
5.2 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, AllCount 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.AllCount += stats.AllCount
|
|
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))
|
|
if err != nil {
|
|
return stats, err
|
|
}
|
|
|
|
stats.AllCount = stats.OpenCount + stats.ClosedCount
|
|
|
|
return stats, nil
|
|
}
|
|
|
|
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()
|
|
}
|