mirror of
https://github.com/hanzoai/forge.git
synced 2026-10-30 15:00:39 +00:00
Source files keep their SPDX-License-Identifier and Hanzo copyright line; the Gitea, Gogs and LFS Test Server copyrights they used to repeat now live once in NOTICE, alongside LICENSE. The header lint accepts any copyright line above the SPDX line, so it no longer demands an upstream one. Co-authored-by: Hanzo Dev <[email protected]>
180 lines
4.8 KiB
Go
180 lines
4.8 KiB
Go
// Copyright 2026 Hanzo AI, Inc. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package issues
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"github.com/hanzoai/git/models/db"
|
|
|
|
"github.com/hanzoai/builder"
|
|
)
|
|
|
|
// 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
|
|
)
|
|
|
|
// 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
|
|
var 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 := min(i+MaxQueryParameters, 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.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))
|
|
return stats, err
|
|
}
|
|
|
|
func applyIssuesOptions(sess db.Session, opts *IssuesOptions, issueIDs []int64) db.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)
|
|
|
|
applyAssigneeCondition(sess, opts.AssigneeID)
|
|
|
|
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()
|
|
}
|