aboutsummaryrefslogtreecommitdiffstats
path: root/models/issues
diff options
context:
space:
mode:
authorLunny Xiao <xiaolunwen@gmail.com>2023-05-19 22:17:48 +0800
committerGitHub <noreply@github.com>2023-05-19 22:17:48 +0800
commit38cf43d0606c13c38f459659f38e26cf31dceccb (patch)
tree53cae445e91b7448ae37a00a7ae398aa082f958d /models/issues
parentc757765a9e5c2d4f73b1a7c3debe3548c735bd54 (diff)
downloadgitea-38cf43d0606c13c38f459659f38e26cf31dceccb.tar.gz
gitea-38cf43d0606c13c38f459659f38e26cf31dceccb.zip
Some refactors for issues stats (#24793)
This PR - [x] Move some functions from `issues.go` to `issue_stats.go` and `issue_label.go` - [x] Remove duplicated issue options `UserIssueStatsOption` to keep only one `IssuesOptions`
Diffstat (limited to 'models/issues')
-rw-r--r--models/issues/issue.go192
-rw-r--r--models/issues/issue_label.go490
-rw-r--r--models/issues/issue_search.go383
-rw-r--r--models/issues/issue_stats.go383
-rw-r--r--models/issues/issue_test.go69
-rw-r--r--models/issues/issue_update.go2
-rw-r--r--models/issues/label.go337
7 files changed, 930 insertions, 926 deletions
diff --git a/models/issues/issue.go b/models/issues/issue.go
index 8dc0381e02..bf41a7ec28 100644
--- a/models/issues/issue.go
+++ b/models/issues/issue.go
@@ -8,10 +8,8 @@ import (
"context"
"fmt"
"regexp"
- "sort"
"code.gitea.io/gitea/models/db"
- access_model "code.gitea.io/gitea/models/perm/access"
project_model "code.gitea.io/gitea/models/project"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
@@ -212,17 +210,6 @@ func (issue *Issue) GetPullRequest() (pr *PullRequest, err error) {
return pr, err
}
-// LoadLabels loads labels
-func (issue *Issue) LoadLabels(ctx context.Context) (err error) {
- if issue.Labels == nil && issue.ID != 0 {
- issue.Labels, err = GetLabelsByIssueID(ctx, issue.ID)
- if err != nil {
- return fmt.Errorf("getLabelsByIssueID [%d]: %w", issue.ID, err)
- }
- }
- return nil
-}
-
// LoadPoster loads poster
func (issue *Issue) LoadPoster(ctx context.Context) (err error) {
if issue.Poster == nil && issue.PosterID != 0 {
@@ -459,175 +446,6 @@ func (issue *Issue) IsPoster(uid int64) bool {
return issue.OriginalAuthorID == 0 && issue.PosterID == uid
}
-func (issue *Issue) getLabels(ctx context.Context) (err error) {
- if len(issue.Labels) > 0 {
- return nil
- }
-
- issue.Labels, err = GetLabelsByIssueID(ctx, issue.ID)
- if err != nil {
- return fmt.Errorf("getLabelsByIssueID: %w", err)
- }
- return nil
-}
-
-func clearIssueLabels(ctx context.Context, issue *Issue, doer *user_model.User) (err error) {
- if err = issue.getLabels(ctx); err != nil {
- return fmt.Errorf("getLabels: %w", err)
- }
-
- for i := range issue.Labels {
- if err = deleteIssueLabel(ctx, issue, issue.Labels[i], doer); err != nil {
- return fmt.Errorf("removeLabel: %w", err)
- }
- }
-
- return nil
-}
-
-// ClearIssueLabels removes all issue labels as the given user.
-// Triggers appropriate WebHooks, if any.
-func ClearIssueLabels(issue *Issue, doer *user_model.User) (err error) {
- ctx, committer, err := db.TxContext(db.DefaultContext)
- if err != nil {
- return err
- }
- defer committer.Close()
-
- if err := issue.LoadRepo(ctx); err != nil {
- return err
- } else if err = issue.LoadPullRequest(ctx); err != nil {
- return err
- }
-
- perm, err := access_model.GetUserRepoPermission(ctx, issue.Repo, doer)
- if err != nil {
- return err
- }
- if !perm.CanWriteIssuesOrPulls(issue.IsPull) {
- return ErrRepoLabelNotExist{}
- }
-
- if err = clearIssueLabels(ctx, issue, doer); err != nil {
- return err
- }
-
- if err = committer.Commit(); err != nil {
- return fmt.Errorf("Commit: %w", err)
- }
-
- return nil
-}
-
-type labelSorter []*Label
-
-func (ts labelSorter) Len() int {
- return len([]*Label(ts))
-}
-
-func (ts labelSorter) Less(i, j int) bool {
- return []*Label(ts)[i].ID < []*Label(ts)[j].ID
-}
-
-func (ts labelSorter) Swap(i, j int) {
- []*Label(ts)[i], []*Label(ts)[j] = []*Label(ts)[j], []*Label(ts)[i]
-}
-
-// Ensure only one label of a given scope exists, with labels at the end of the
-// array getting preference over earlier ones.
-func RemoveDuplicateExclusiveLabels(labels []*Label) []*Label {
- validLabels := make([]*Label, 0, len(labels))
-
- for i, label := range labels {
- scope := label.ExclusiveScope()
- if scope != "" {
- foundOther := false
- for _, otherLabel := range labels[i+1:] {
- if otherLabel.ExclusiveScope() == scope {
- foundOther = true
- break
- }
- }
- if foundOther {
- continue
- }
- }
- validLabels = append(validLabels, label)
- }
-
- return validLabels
-}
-
-// ReplaceIssueLabels removes all current labels and add new labels to the issue.
-// Triggers appropriate WebHooks, if any.
-func ReplaceIssueLabels(issue *Issue, labels []*Label, doer *user_model.User) (err error) {
- ctx, committer, err := db.TxContext(db.DefaultContext)
- if err != nil {
- return err
- }
- defer committer.Close()
-
- if err = issue.LoadRepo(ctx); err != nil {
- return err
- }
-
- if err = issue.LoadLabels(ctx); err != nil {
- return err
- }
-
- labels = RemoveDuplicateExclusiveLabels(labels)
-
- sort.Sort(labelSorter(labels))
- sort.Sort(labelSorter(issue.Labels))
-
- var toAdd, toRemove []*Label
-
- addIndex, removeIndex := 0, 0
- for addIndex < len(labels) && removeIndex < len(issue.Labels) {
- addLabel := labels[addIndex]
- removeLabel := issue.Labels[removeIndex]
- if addLabel.ID == removeLabel.ID {
- // Silently drop invalid labels
- if removeLabel.RepoID != issue.RepoID && removeLabel.OrgID != issue.Repo.OwnerID {
- toRemove = append(toRemove, removeLabel)
- }
-
- addIndex++
- removeIndex++
- } else if addLabel.ID < removeLabel.ID {
- // Only add if the label is valid
- if addLabel.RepoID == issue.RepoID || addLabel.OrgID == issue.Repo.OwnerID {
- toAdd = append(toAdd, addLabel)
- }
- addIndex++
- } else {
- toRemove = append(toRemove, removeLabel)
- removeIndex++
- }
- }
- toAdd = append(toAdd, labels[addIndex:]...)
- toRemove = append(toRemove, issue.Labels[removeIndex:]...)
-
- if len(toAdd) > 0 {
- if err = newIssueLabels(ctx, issue, toAdd, doer); err != nil {
- return fmt.Errorf("addLabels: %w", err)
- }
- }
-
- for _, l := range toRemove {
- if err = deleteIssueLabel(ctx, issue, l, doer); err != nil {
- return fmt.Errorf("removeLabel: %w", err)
- }
- }
-
- issue.Labels = nil
- if err = issue.LoadLabels(ctx); err != nil {
- return err
- }
-
- return committer.Commit()
-}
-
// GetTasks returns the amount of tasks in the issues content
func (issue *Issue) GetTasks() int {
return len(issueTasksPat.FindAllStringIndex(issue.Content, -1))
@@ -862,16 +680,6 @@ func (issue *Issue) GetExternalName() string { return issue.OriginalAuthor }
// GetExternalID ExternalUserRemappable interface
func (issue *Issue) GetExternalID() int64 { return issue.OriginalAuthorID }
-// 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()
-}
-
// HasOriginalAuthor returns if an issue was migrated and has an original author.
func (issue *Issue) HasOriginalAuthor() bool {
return issue.OriginalAuthor != "" && issue.OriginalAuthorID != 0
diff --git a/models/issues/issue_label.go b/models/issues/issue_label.go
new file mode 100644
index 0000000000..f4060b1402
--- /dev/null
+++ b/models/issues/issue_label.go
@@ -0,0 +1,490 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package issues
+
+import (
+ "context"
+ "fmt"
+ "sort"
+
+ "code.gitea.io/gitea/models/db"
+ access_model "code.gitea.io/gitea/models/perm/access"
+ user_model "code.gitea.io/gitea/models/user"
+
+ "xorm.io/builder"
+)
+
+// IssueLabel represents an issue-label relation.
+type IssueLabel struct {
+ ID int64 `xorm:"pk autoincr"`
+ IssueID int64 `xorm:"UNIQUE(s)"`
+ LabelID int64 `xorm:"UNIQUE(s)"`
+}
+
+// HasIssueLabel returns true if issue has been labeled.
+func HasIssueLabel(ctx context.Context, issueID, labelID int64) bool {
+ has, _ := db.GetEngine(ctx).Where("issue_id = ? AND label_id = ?", issueID, labelID).Get(new(IssueLabel))
+ return has
+}
+
+// newIssueLabel this function creates a new label it does not check if the label is valid for the issue
+// YOU MUST CHECK THIS BEFORE THIS FUNCTION
+func newIssueLabel(ctx context.Context, issue *Issue, label *Label, doer *user_model.User) (err error) {
+ if err = db.Insert(ctx, &IssueLabel{
+ IssueID: issue.ID,
+ LabelID: label.ID,
+ }); err != nil {
+ return err
+ }
+
+ if err = issue.LoadRepo(ctx); err != nil {
+ return
+ }
+
+ opts := &CreateCommentOptions{
+ Type: CommentTypeLabel,
+ Doer: doer,
+ Repo: issue.Repo,
+ Issue: issue,
+ Label: label,
+ Content: "1",
+ }
+ if _, err = CreateComment(ctx, opts); err != nil {
+ return err
+ }
+
+ return updateLabelCols(ctx, label, "num_issues", "num_closed_issue")
+}
+
+// Remove all issue labels in the given exclusive scope
+func RemoveDuplicateExclusiveIssueLabels(ctx context.Context, issue *Issue, label *Label, doer *user_model.User) (err error) {
+ scope := label.ExclusiveScope()
+ if scope == "" {
+ return nil
+ }
+
+ var toRemove []*Label
+ for _, issueLabel := range issue.Labels {
+ if label.ID != issueLabel.ID && issueLabel.ExclusiveScope() == scope {
+ toRemove = append(toRemove, issueLabel)
+ }
+ }
+
+ for _, issueLabel := range toRemove {
+ if err = deleteIssueLabel(ctx, issue, issueLabel, doer); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+// NewIssueLabel creates a new issue-label relation.
+func NewIssueLabel(issue *Issue, label *Label, doer *user_model.User) (err error) {
+ if HasIssueLabel(db.DefaultContext, issue.ID, label.ID) {
+ return nil
+ }
+
+ ctx, committer, err := db.TxContext(db.DefaultContext)
+ if err != nil {
+ return err
+ }
+ defer committer.Close()
+
+ if err = issue.LoadRepo(ctx); err != nil {
+ return err
+ }
+
+ // Do NOT add invalid labels
+ if issue.RepoID != label.RepoID && issue.Repo.OwnerID != label.OrgID {
+ return nil
+ }
+
+ if err = RemoveDuplicateExclusiveIssueLabels(ctx, issue, label, doer); err != nil {
+ return nil
+ }
+
+ if err = newIssueLabel(ctx, issue, label, doer); err != nil {
+ return err
+ }
+
+ issue.Labels = nil
+ if err = issue.LoadLabels(ctx); err != nil {
+ return err
+ }
+
+ return committer.Commit()
+}
+
+// newIssueLabels add labels to an issue. It will check if the labels are valid for the issue
+func newIssueLabels(ctx context.Context, issue *Issue, labels []*Label, doer *user_model.User) (err error) {
+ if err = issue.LoadRepo(ctx); err != nil {
+ return err
+ }
+ for _, l := range labels {
+ // Don't add already present labels and invalid labels
+ if HasIssueLabel(ctx, issue.ID, l.ID) ||
+ (l.RepoID != issue.RepoID && l.OrgID != issue.Repo.OwnerID) {
+ continue
+ }
+
+ if err = newIssueLabel(ctx, issue, l, doer); err != nil {
+ return fmt.Errorf("newIssueLabel: %w", err)
+ }
+ }
+
+ return nil
+}
+
+// NewIssueLabels creates a list of issue-label relations.
+func NewIssueLabels(issue *Issue, labels []*Label, doer *user_model.User) (err error) {
+ ctx, committer, err := db.TxContext(db.DefaultContext)
+ if err != nil {
+ return err
+ }
+ defer committer.Close()
+
+ if err = newIssueLabels(ctx, issue, labels, doer); err != nil {
+ return err
+ }
+
+ issue.Labels = nil
+ if err = issue.LoadLabels(ctx); err != nil {
+ return err
+ }
+
+ return committer.Commit()
+}
+
+func deleteIssueLabel(ctx context.Context, issue *Issue, label *Label, doer *user_model.User) (err error) {
+ if count, err := db.DeleteByBean(ctx, &IssueLabel{
+ IssueID: issue.ID,
+ LabelID: label.ID,
+ }); err != nil {
+ return err
+ } else if count == 0 {
+ return nil
+ }
+
+ if err = issue.LoadRepo(ctx); err != nil {
+ return
+ }
+
+ opts := &CreateCommentOptions{
+ Type: CommentTypeLabel,
+ Doer: doer,
+ Repo: issue.Repo,
+ Issue: issue,
+ Label: label,
+ }
+ if _, err = CreateComment(ctx, opts); err != nil {
+ return err
+ }
+
+ return updateLabelCols(ctx, label, "num_issues", "num_closed_issue")
+}
+
+// DeleteIssueLabel deletes issue-label relation.
+func DeleteIssueLabel(ctx context.Context, issue *Issue, label *Label, doer *user_model.User) error {
+ if err := deleteIssueLabel(ctx, issue, label, doer); err != nil {
+ return err
+ }
+
+ issue.Labels = nil
+ return issue.LoadLabels(ctx)
+}
+
+// DeleteLabelsByRepoID deletes labels of some repository
+func DeleteLabelsByRepoID(ctx context.Context, repoID int64) error {
+ deleteCond := builder.Select("id").From("label").Where(builder.Eq{"label.repo_id": repoID})
+
+ if _, err := db.GetEngine(ctx).In("label_id", deleteCond).
+ Delete(&IssueLabel{}); err != nil {
+ return err
+ }
+
+ _, err := db.DeleteByBean(ctx, &Label{RepoID: repoID})
+ return err
+}
+
+// CountOrphanedLabels return count of labels witch are broken and not accessible via ui anymore
+func CountOrphanedLabels(ctx context.Context) (int64, error) {
+ noref, err := db.GetEngine(ctx).Table("label").Where("repo_id=? AND org_id=?", 0, 0).Count()
+ if err != nil {
+ return 0, err
+ }
+
+ norepo, err := db.GetEngine(ctx).Table("label").
+ Where(builder.And(
+ builder.Gt{"repo_id": 0},
+ builder.NotIn("repo_id", builder.Select("id").From("`repository`")),
+ )).
+ Count()
+ if err != nil {
+ return 0, err
+ }
+
+ noorg, err := db.GetEngine(ctx).Table("label").
+ Where(builder.And(
+ builder.Gt{"org_id": 0},
+ builder.NotIn("org_id", builder.Select("id").From("`user`")),
+ )).
+ Count()
+ if err != nil {
+ return 0, err
+ }
+
+ return noref + norepo + noorg, nil
+}
+
+// DeleteOrphanedLabels delete labels witch are broken and not accessible via ui anymore
+func DeleteOrphanedLabels(ctx context.Context) error {
+ // delete labels with no reference
+ if _, err := db.GetEngine(ctx).Table("label").Where("repo_id=? AND org_id=?", 0, 0).Delete(new(Label)); err != nil {
+ return err
+ }
+
+ // delete labels with none existing repos
+ if _, err := db.GetEngine(ctx).
+ Where(builder.And(
+ builder.Gt{"repo_id": 0},
+ builder.NotIn("repo_id", builder.Select("id").From("`repository`")),
+ )).
+ Delete(Label{}); err != nil {
+ return err
+ }
+
+ // delete labels with none existing orgs
+ if _, err := db.GetEngine(ctx).
+ Where(builder.And(
+ builder.Gt{"org_id": 0},
+ builder.NotIn("org_id", builder.Select("id").From("`user`")),
+ )).
+ Delete(Label{}); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// CountOrphanedIssueLabels return count of IssueLabels witch have no label behind anymore
+func CountOrphanedIssueLabels(ctx context.Context) (int64, error) {
+ return db.GetEngine(ctx).Table("issue_label").
+ NotIn("label_id", builder.Select("id").From("label")).
+ Count()
+}
+
+// DeleteOrphanedIssueLabels delete IssueLabels witch have no label behind anymore
+func DeleteOrphanedIssueLabels(ctx context.Context) error {
+ _, err := db.GetEngine(ctx).
+ NotIn("label_id", builder.Select("id").From("label")).
+ Delete(IssueLabel{})
+ return err
+}
+
+// CountIssueLabelWithOutsideLabels count label comments with outside label
+func CountIssueLabelWithOutsideLabels(ctx context.Context) (int64, error) {
+ return db.GetEngine(ctx).Where(builder.Expr("(label.org_id = 0 AND issue.repo_id != label.repo_id) OR (label.repo_id = 0 AND label.org_id != repository.owner_id)")).
+ Table("issue_label").
+ Join("inner", "label", "issue_label.label_id = label.id ").
+ Join("inner", "issue", "issue.id = issue_label.issue_id ").
+ Join("inner", "repository", "issue.repo_id = repository.id").
+ Count(new(IssueLabel))
+}
+
+// FixIssueLabelWithOutsideLabels fix label comments with outside label
+func FixIssueLabelWithOutsideLabels(ctx context.Context) (int64, error) {
+ res, err := db.GetEngine(ctx).Exec(`DELETE FROM issue_label WHERE issue_label.id IN (
+ SELECT il_too.id FROM (
+ SELECT il_too_too.id
+ FROM issue_label AS il_too_too
+ INNER JOIN label ON il_too_too.label_id = label.id
+ INNER JOIN issue on issue.id = il_too_too.issue_id
+ INNER JOIN repository on repository.id = issue.repo_id
+ WHERE
+ (label.org_id = 0 AND issue.repo_id != label.repo_id) OR (label.repo_id = 0 AND label.org_id != repository.owner_id)
+ ) AS il_too )`)
+ if err != nil {
+ return 0, err
+ }
+
+ return res.RowsAffected()
+}
+
+// LoadLabels loads labels
+func (issue *Issue) LoadLabels(ctx context.Context) (err error) {
+ if issue.Labels == nil && issue.ID != 0 {
+ issue.Labels, err = GetLabelsByIssueID(ctx, issue.ID)
+ if err != nil {
+ return fmt.Errorf("getLabelsByIssueID [%d]: %w", issue.ID, err)
+ }
+ }
+ return nil
+}
+
+// GetLabelsByIssueID returns all labels that belong to given issue by ID.
+func GetLabelsByIssueID(ctx context.Context, issueID int64) ([]*Label, error) {
+ var labels []*Label
+ return labels, db.GetEngine(ctx).Where("issue_label.issue_id = ?", issueID).
+ Join("LEFT", "issue_label", "issue_label.label_id = label.id").
+ Asc("label.name").
+ Find(&labels)
+}
+
+func clearIssueLabels(ctx context.Context, issue *Issue, doer *user_model.User) (err error) {
+ if err = issue.LoadLabels(ctx); err != nil {
+ return fmt.Errorf("getLabels: %w", err)
+ }
+
+ for i := range issue.Labels {
+ if err = deleteIssueLabel(ctx, issue, issue.Labels[i], doer); err != nil {
+ return fmt.Errorf("removeLabel: %w", err)
+ }
+ }
+
+ return nil
+}
+
+// ClearIssueLabels removes all issue labels as the given user.
+// Triggers appropriate WebHooks, if any.
+func ClearIssueLabels(issue *Issue, doer *user_model.User) (err error) {
+ ctx, committer, err := db.TxContext(db.DefaultContext)
+ if err != nil {
+ return err
+ }
+ defer committer.Close()
+
+ if err := issue.LoadRepo(ctx); err != nil {
+ return err
+ } else if err = issue.LoadPullRequest(ctx); err != nil {
+ return err
+ }
+
+ perm, err := access_model.GetUserRepoPermission(ctx, issue.Repo, doer)
+ if err != nil {
+ return err
+ }
+ if !perm.CanWriteIssuesOrPulls(issue.IsPull) {
+ return ErrRepoLabelNotExist{}
+ }
+
+ if err = clearIssueLabels(ctx, issue, doer); err != nil {
+ return err
+ }
+
+ if err = committer.Commit(); err != nil {
+ return fmt.Errorf("Commit: %w", err)
+ }
+
+ return nil
+}
+
+type labelSorter []*Label
+
+func (ts labelSorter) Len() int {
+ return len([]*Label(ts))
+}
+
+func (ts labelSorter) Less(i, j int) bool {
+ return []*Label(ts)[i].ID < []*Label(ts)[j].ID
+}
+
+func (ts labelSorter) Swap(i, j int) {
+ []*Label(ts)[i], []*Label(ts)[j] = []*Label(ts)[j], []*Label(ts)[i]
+}
+
+// Ensure only one label of a given scope exists, with labels at the end of the
+// array getting preference over earlier ones.
+func RemoveDuplicateExclusiveLabels(labels []*Label) []*Label {
+ validLabels := make([]*Label, 0, len(labels))
+
+ for i, label := range labels {
+ scope := label.ExclusiveScope()
+ if scope != "" {
+ foundOther := false
+ for _, otherLabel := range labels[i+1:] {
+ if otherLabel.ExclusiveScope() == scope {
+ foundOther = true
+ break
+ }
+ }
+ if foundOther {
+ continue
+ }
+ }
+ validLabels = append(validLabels, label)
+ }
+
+ return validLabels
+}
+
+// ReplaceIssueLabels removes all current labels and add new labels to the issue.
+// Triggers appropriate WebHooks, if any.
+func ReplaceIssueLabels(issue *Issue, labels []*Label, doer *user_model.User) (err error) {
+ ctx, committer, err := db.TxContext(db.DefaultContext)
+ if err != nil {
+ return err
+ }
+ defer committer.Close()
+
+ if err = issue.LoadRepo(ctx); err != nil {
+ return err
+ }
+
+ if err = issue.LoadLabels(ctx); err != nil {
+ return err
+ }
+
+ labels = RemoveDuplicateExclusiveLabels(labels)
+
+ sort.Sort(labelSorter(labels))
+ sort.Sort(labelSorter(issue.Labels))
+
+ var toAdd, toRemove []*Label
+
+ addIndex, removeIndex := 0, 0
+ for addIndex < len(labels) && removeIndex < len(issue.Labels) {
+ addLabel := labels[addIndex]
+ removeLabel := issue.Labels[removeIndex]
+ if addLabel.ID == removeLabel.ID {
+ // Silently drop invalid labels
+ if removeLabel.RepoID != issue.RepoID && removeLabel.OrgID != issue.Repo.OwnerID {
+ toRemove = append(toRemove, removeLabel)
+ }
+
+ addIndex++
+ removeIndex++
+ } else if addLabel.ID < removeLabel.ID {
+ // Only add if the label is valid
+ if addLabel.RepoID == issue.RepoID || addLabel.OrgID == issue.Repo.OwnerID {
+ toAdd = append(toAdd, addLabel)
+ }
+ addIndex++
+ } else {
+ toRemove = append(toRemove, removeLabel)
+ removeIndex++
+ }
+ }
+ toAdd = append(toAdd, labels[addIndex:]...)
+ toRemove = append(toRemove, issue.Labels[removeIndex:]...)
+
+ if len(toAdd) > 0 {
+ if err = newIssueLabels(ctx, issue, toAdd, doer); err != nil {
+ return fmt.Errorf("addLabels: %w", err)
+ }
+ }
+
+ for _, l := range toRemove {
+ if err = deleteIssueLabel(ctx, issue, l, doer); err != nil {
+ return fmt.Errorf("removeLabel: %w", err)
+ }
+ }
+
+ issue.Labels = nil
+ if err = issue.LoadLabels(ctx); err != nil {
+ return err
+ }
+
+ return committer.Commit()
+}
diff --git a/models/issues/issue_search.go b/models/issues/issue_search.go
index e01070ecae..9fd13f0995 100644
--- a/models/issues/issue_search.go
+++ b/models/issues/issue_search.go
@@ -22,7 +22,7 @@ import (
// IssuesOptions represents options of an issue.
type IssuesOptions struct { //nolint
db.ListOptions
- RepoID int64 // overwrites RepoCond if not 0
+ RepoIDs []int64 // overwrites RepoCond if the length is not 0
RepoCond builder.Cond
AssigneeID int64
PosterID int64
@@ -155,17 +155,24 @@ func applyMilestoneCondition(sess *xorm.Session, opts *IssuesOptions) *xorm.Sess
return sess
}
+func applyRepoConditions(sess *xorm.Session, opts *IssuesOptions) *xorm.Session {
+ if len(opts.RepoIDs) == 1 {
+ opts.RepoCond = builder.Eq{"issue.repo_id": opts.RepoIDs[0]}
+ } else if len(opts.RepoIDs) > 1 {
+ opts.RepoCond = builder.In("issue.repo_id", opts.RepoIDs)
+ }
+ if opts.RepoCond != nil {
+ sess.And(opts.RepoCond)
+ }
+ return sess
+}
+
func applyConditions(sess *xorm.Session, opts *IssuesOptions) *xorm.Session {
if len(opts.IssueIDs) > 0 {
sess.In("issue.id", opts.IssueIDs)
}
- if opts.RepoID != 0 {
- opts.RepoCond = builder.Eq{"issue.repo_id": opts.RepoID}
- }
- if opts.RepoCond != nil {
- sess.And(opts.RepoCond)
- }
+ applyRepoConditions(sess, opts)
if !opts.IsClosed.IsNone() {
sess.And("issue.is_closed=?", opts.IsClosed.IsTrue())
@@ -400,31 +407,6 @@ func applySubscribedCondition(sess *xorm.Session, subscriberID int64) *xorm.Sess
)
}
-// 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
-}
-
// GetRepoIDsForIssuesOptions find all repo ids for the given options
func GetRepoIDsForIssuesOptions(opts *IssuesOptions, user *user_model.User) ([]int64, error) {
repoIDs := make([]int64, 0, 5)
@@ -453,351 +435,18 @@ func Issues(ctx context.Context, opts *IssuesOptions) ([]*Issue, error) {
applyConditions(sess, opts)
applySorts(sess, opts.SortType, opts.PriorityRepoID)
- issues := make([]*Issue, 0, opts.ListOptions.PageSize)
+ issues := make(IssueList, 0, opts.ListOptions.PageSize)
if err := sess.Find(&issues); err != nil {
return nil, fmt.Errorf("unable to query Issues: %w", err)
}
- if err := IssueList(issues).LoadAttributes(); err != nil {
+ if err := issues.LoadAttributes(); err != nil {
return nil, fmt.Errorf("unable to LoadAttributes for Issues: %w", err)
}
return issues, 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()
-}
-
-// 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
-)
-
-// 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).
- Where("issue.repo_id = ?", opts.RepoID)
-
- if len(issueIDs) > 0 {
- sess.In("issue.id", issueIDs)
- }
-
- applyLabelsCondition(sess, opts)
-
- applyMilestoneCondition(sess, opts)
-
- if opts.ProjectID > 0 {
- sess.Join("INNER", "project_issue", "issue.id = project_issue.issue_id").
- And("project_issue.project_id=?", opts.ProjectID)
- }
-
- if opts.AssigneeID > 0 {
- applyAssigneeCondition(sess, opts.AssigneeID)
- } else if opts.AssigneeID == db.NoConditionID {
- sess.Where("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
-}
-
-// UserIssueStatsOptions contains parameters accepted by GetUserIssueStats.
-type UserIssueStatsOptions struct {
- UserID int64
- RepoIDs []int64
- FilterMode int
- IsPull bool
- IsClosed bool
- IssueIDs []int64
- IsArchived util.OptionalBool
- LabelIDs []int64
- RepoCond builder.Cond
- Org *organization.Organization
- Team *organization.Team
-}
-
-// GetUserIssueStats returns issue statistic information for dashboard by given conditions.
-func GetUserIssueStats(opts UserIssueStatsOptions) (*IssueStats, error) {
- var err error
- stats := &IssueStats{}
-
- cond := builder.NewCond()
- cond = cond.And(builder.Eq{"issue.is_pull": opts.IsPull})
- 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.UserID > 0 {
- cond = cond.And(issuePullAccessibleRepoCond("issue.repo_id", opts.UserID, opts.Org, opts.Team, opts.IsPull))
- }
-
- sess := func(cond builder.Cond) *xorm.Session {
- s := db.GetEngine(db.DefaultContext).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.UserID > 0 || opts.IsArchived != util.OptionalBoolNone {
- s.Join("INNER", "repository", "issue.repo_id = repository.id")
- if opts.IsArchived != util.OptionalBoolNone {
- s.And(builder.Eq{"repository.is_archived": opts.IsArchived.IsTrue()})
- }
- }
- return s
- }
-
- switch opts.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.UserID).
- And("issue.is_closed = ?", false).
- Count(new(Issue))
- if err != nil {
- return nil, err
- }
- stats.ClosedCount, err = applyAssigneeCondition(sess(cond), opts.UserID).
- And("issue.is_closed = ?", true).
- Count(new(Issue))
- if err != nil {
- return nil, err
- }
- case FilterModeCreate:
- stats.OpenCount, err = applyPosterCondition(sess(cond), opts.UserID).
- And("issue.is_closed = ?", false).
- Count(new(Issue))
- if err != nil {
- return nil, err
- }
- stats.ClosedCount, err = applyPosterCondition(sess(cond), opts.UserID).
- And("issue.is_closed = ?", true).
- Count(new(Issue))
- if err != nil {
- return nil, err
- }
- case FilterModeMention:
- stats.OpenCount, err = applyMentionedCondition(sess(cond), opts.UserID).
- And("issue.is_closed = ?", false).
- Count(new(Issue))
- if err != nil {
- return nil, err
- }
- stats.ClosedCount, err = applyMentionedCondition(sess(cond), opts.UserID).
- And("issue.is_closed = ?", true).
- Count(new(Issue))
- if err != nil {
- return nil, err
- }
- case FilterModeReviewRequested:
- stats.OpenCount, err = applyReviewRequestedCondition(sess(cond), opts.UserID).
- And("issue.is_closed = ?", false).
- Count(new(Issue))
- if err != nil {
- return nil, err
- }
- stats.ClosedCount, err = applyReviewRequestedCondition(sess(cond), opts.UserID).
- And("issue.is_closed = ?", true).
- Count(new(Issue))
- if err != nil {
- return nil, err
- }
- case FilterModeReviewed:
- stats.OpenCount, err = applyReviewedCondition(sess(cond), opts.UserID).
- And("issue.is_closed = ?", false).
- Count(new(Issue))
- if err != nil {
- return nil, err
- }
- stats.ClosedCount, err = applyReviewedCondition(sess(cond), opts.UserID).
- And("issue.is_closed = ?", true).
- Count(new(Issue))
- if err != nil {
- return nil, err
- }
- }
-
- cond = cond.And(builder.Eq{"issue.is_closed": opts.IsClosed})
- stats.AssignCount, err = applyAssigneeCondition(sess(cond), opts.UserID).Count(new(Issue))
- if err != nil {
- return nil, err
- }
-
- stats.CreateCount, err = applyPosterCondition(sess(cond), opts.UserID).Count(new(Issue))
- if err != nil {
- return nil, err
- }
-
- stats.MentionCount, err = applyMentionedCondition(sess(cond), opts.UserID).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.UserID).Count(new(Issue))
- if err != nil {
- return nil, err
- }
-
- stats.ReviewedCount, err = applyReviewedCondition(sess(cond), opts.UserID).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
-}
-
// SearchIssueIDsByKeyword search issues on database
func SearchIssueIDsByKeyword(ctx context.Context, kw string, repoIDs []int64, limit, start int) (int64, []int64, error) {
repoCond := builder.In("repo_id", repoIDs)
diff --git a/models/issues/issue_stats.go b/models/issues/issue_stats.go
new file mode 100644
index 0000000000..9b9562ebdd
--- /dev/null
+++ b/models/issues/issue_stats.go
@@ -0,0 +1,383 @@
+// 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)
+
+ if opts.ProjectID > 0 {
+ sess.Join("INNER", "project_issue", "issue.id = project_issue.issue_id").
+ And("project_issue.project_id=?", opts.ProjectID)
+ }
+
+ if opts.AssigneeID > 0 {
+ applyAssigneeCondition(sess, opts.AssigneeID)
+ } else if opts.AssigneeID == db.NoConditionID {
+ sess.Where("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()
+}
diff --git a/models/issues/issue_test.go b/models/issues/issue_test.go
index 5bf2f819be..80699a57b4 100644
--- a/models/issues/issue_test.go
+++ b/models/issues/issue_test.go
@@ -17,6 +17,7 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/util"
"github.com/stretchr/testify/assert"
"xorm.io/builder"
@@ -204,14 +205,16 @@ func TestIssues(t *testing.T) {
func TestGetUserIssueStats(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
for _, test := range []struct {
- Opts issues_model.UserIssueStatsOptions
+ FilterMode int
+ Opts issues_model.IssuesOptions
ExpectedIssueStats issues_model.IssueStats
}{
{
- issues_model.UserIssueStatsOptions{
- UserID: 1,
- RepoIDs: []int64{1},
- FilterMode: issues_model.FilterModeAll,
+ issues_model.FilterModeAll,
+ issues_model.IssuesOptions{
+ User: unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}),
+ RepoIDs: []int64{1},
+ IsPull: util.OptionalBoolFalse,
},
issues_model.IssueStats{
YourRepositoriesCount: 1, // 6
@@ -222,11 +225,12 @@ func TestGetUserIssueStats(t *testing.T) {
},
},
{
- issues_model.UserIssueStatsOptions{
- UserID: 1,
- RepoIDs: []int64{1},
- FilterMode: issues_model.FilterModeAll,
- IsClosed: true,
+ issues_model.FilterModeAll,
+ issues_model.IssuesOptions{
+ User: unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}),
+ RepoIDs: []int64{1},
+ IsPull: util.OptionalBoolFalse,
+ IsClosed: util.OptionalBoolTrue,
},
issues_model.IssueStats{
YourRepositoriesCount: 1, // 6
@@ -237,9 +241,10 @@ func TestGetUserIssueStats(t *testing.T) {
},
},
{
- issues_model.UserIssueStatsOptions{
- UserID: 1,
- FilterMode: issues_model.FilterModeAssign,
+ issues_model.FilterModeAssign,
+ issues_model.IssuesOptions{
+ User: unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}),
+ IsPull: util.OptionalBoolFalse,
},
issues_model.IssueStats{
YourRepositoriesCount: 1, // 6
@@ -250,9 +255,10 @@ func TestGetUserIssueStats(t *testing.T) {
},
},
{
- issues_model.UserIssueStatsOptions{
- UserID: 1,
- FilterMode: issues_model.FilterModeCreate,
+ issues_model.FilterModeCreate,
+ issues_model.IssuesOptions{
+ User: unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}),
+ IsPull: util.OptionalBoolFalse,
},
issues_model.IssueStats{
YourRepositoriesCount: 1, // 6
@@ -263,9 +269,10 @@ func TestGetUserIssueStats(t *testing.T) {
},
},
{
- issues_model.UserIssueStatsOptions{
- UserID: 1,
- FilterMode: issues_model.FilterModeMention,
+ issues_model.FilterModeMention,
+ issues_model.IssuesOptions{
+ User: unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}),
+ IsPull: util.OptionalBoolFalse,
},
issues_model.IssueStats{
YourRepositoriesCount: 1, // 6
@@ -277,10 +284,11 @@ func TestGetUserIssueStats(t *testing.T) {
},
},
{
- issues_model.UserIssueStatsOptions{
- UserID: 1,
- FilterMode: issues_model.FilterModeCreate,
- IssueIDs: []int64{1},
+ issues_model.FilterModeCreate,
+ issues_model.IssuesOptions{
+ User: unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}),
+ IssueIDs: []int64{1},
+ IsPull: util.OptionalBoolFalse,
},
issues_model.IssueStats{
YourRepositoriesCount: 1, // 1
@@ -291,11 +299,12 @@ func TestGetUserIssueStats(t *testing.T) {
},
},
{
- issues_model.UserIssueStatsOptions{
- UserID: 2,
- Org: unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3}),
- Team: unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 7}),
- FilterMode: issues_model.FilterModeAll,
+ issues_model.FilterModeAll,
+ issues_model.IssuesOptions{
+ User: unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}),
+ Org: unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3}),
+ Team: unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 7}),
+ IsPull: util.OptionalBoolFalse,
},
issues_model.IssueStats{
YourRepositoriesCount: 2,
@@ -306,7 +315,7 @@ func TestGetUserIssueStats(t *testing.T) {
},
} {
t.Run(fmt.Sprintf("%#v", test.Opts), func(t *testing.T) {
- stats, err := issues_model.GetUserIssueStats(test.Opts)
+ stats, err := issues_model.GetUserIssueStats(test.FilterMode, test.Opts)
if !assert.NoError(t, err) {
return
}
@@ -495,7 +504,7 @@ func TestCorrectIssueStats(t *testing.T) {
// Now we will call the GetIssueStats with these IDs and if working,
// get the correct stats back.
issueStats, err := issues_model.GetIssueStats(&issues_model.IssuesOptions{
- RepoID: 1,
+ RepoIDs: []int64{1},
IssueIDs: ids,
})
diff --git a/models/issues/issue_update.go b/models/issues/issue_update.go
index bebd5f4cd6..b6fd720fe5 100644
--- a/models/issues/issue_update.go
+++ b/models/issues/issue_update.go
@@ -81,7 +81,7 @@ func doChangeIssueStatus(ctx context.Context, issue *Issue, doer *user_model.Use
}
// Update issue count of labels
- if err := issue.getLabels(ctx); err != nil {
+ if err := issue.LoadLabels(ctx); err != nil {
return nil, err
}
for idx := range issue.Labels {
diff --git a/models/issues/label.go b/models/issues/label.go
index 9c22dcdd2d..8f2cf05a28 100644
--- a/models/issues/label.go
+++ b/models/issues/label.go
@@ -11,7 +11,6 @@ import (
"strings"
"code.gitea.io/gitea/models/db"
- user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/label"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
@@ -113,7 +112,7 @@ func (l *Label) CalOpenIssues() {
// CalOpenOrgIssues calculates the open issues of a label for a specific repo
func (l *Label) CalOpenOrgIssues(ctx context.Context, repoID, labelID int64) {
counts, _ := CountIssuesByRepo(ctx, &IssuesOptions{
- RepoID: repoID,
+ RepoIDs: []int64{repoID},
LabelIDs: []int64{labelID},
IsClosed: util.OptionalBoolFalse,
})
@@ -282,13 +281,6 @@ func GetLabelsByIDs(labelIDs []int64) ([]*Label, error) {
Find(&labels)
}
-// __________ .__ __
-// \______ \ ____ ______ ____ _____|__|/ |_ ___________ ___.__.
-// | _// __ \\____ \ / _ \/ ___/ \ __\/ _ \_ __ < | |
-// | | \ ___/| |_> > <_> )___ \| || | ( <_> ) | \/\___ |
-// |____|_ /\___ > __/ \____/____ >__||__| \____/|__| / ____|
-// \/ \/|__| \/ \/
-
// GetLabelInRepoByName returns a label by name in given repository.
func GetLabelInRepoByName(ctx context.Context, repoID int64, labelName string) (*Label, error) {
if len(labelName) == 0 || repoID <= 0 {
@@ -393,13 +385,6 @@ func CountLabelsByRepoID(repoID int64) (int64, error) {
return db.GetEngine(db.DefaultContext).Where("repo_id = ?", repoID).Count(&Label{})
}
-// ________
-// \_____ \_______ ____
-// / | \_ __ \/ ___\
-// / | \ | \/ /_/ >
-// \_______ /__| \___ /
-// \/ /_____/
-
// GetLabelInOrgByName returns a label by name in given organization.
func GetLabelInOrgByName(ctx context.Context, orgID int64, labelName string) (*Label, error) {
if len(labelName) == 0 || orgID <= 0 {
@@ -496,22 +481,6 @@ func CountLabelsByOrgID(orgID int64) (int64, error) {
return db.GetEngine(db.DefaultContext).Where("org_id = ?", orgID).Count(&Label{})
}
-// .___
-// | | ______ ________ __ ____
-// | |/ ___// ___/ | \_/ __ \
-// | |\___ \ \___ \| | /\ ___/
-// |___/____ >____ >____/ \___ |
-// \/ \/ \/
-
-// GetLabelsByIssueID returns all labels that belong to given issue by ID.
-func GetLabelsByIssueID(ctx context.Context, issueID int64) ([]*Label, error) {
- var labels []*Label
- return labels, db.GetEngine(ctx).Where("issue_label.issue_id = ?", issueID).
- Join("LEFT", "issue_label", "issue_label.label_id = label.id").
- Asc("label.name").
- Find(&labels)
-}
-
func updateLabelCols(ctx context.Context, l *Label, cols ...string) error {
_, err := db.GetEngine(ctx).ID(l.ID).
SetExpr("num_issues",
@@ -529,307 +498,3 @@ func updateLabelCols(ctx context.Context, l *Label, cols ...string) error {
Cols(cols...).Update(l)
return err
}
-
-// .___ .____ ___. .__
-// | | ______ ________ __ ____ | | _____ \_ |__ ____ | |
-// | |/ ___// ___/ | \_/ __ \| | \__ \ | __ \_/ __ \| |
-// | |\___ \ \___ \| | /\ ___/| |___ / __ \| \_\ \ ___/| |__
-// |___/____ >____ >____/ \___ >_______ (____ /___ /\___ >____/
-// \/ \/ \/ \/ \/ \/ \/
-
-// IssueLabel represents an issue-label relation.
-type IssueLabel struct {
- ID int64 `xorm:"pk autoincr"`
- IssueID int64 `xorm:"UNIQUE(s)"`
- LabelID int64 `xorm:"UNIQUE(s)"`
-}
-
-// HasIssueLabel returns true if issue has been labeled.
-func HasIssueLabel(ctx context.Context, issueID, labelID int64) bool {
- has, _ := db.GetEngine(ctx).Where("issue_id = ? AND label_id = ?", issueID, labelID).Get(new(IssueLabel))
- return has
-}
-
-// newIssueLabel this function creates a new label it does not check if the label is valid for the issue
-// YOU MUST CHECK THIS BEFORE THIS FUNCTION
-func newIssueLabel(ctx context.Context, issue *Issue, label *Label, doer *user_model.User) (err error) {
- if err = db.Insert(ctx, &IssueLabel{
- IssueID: issue.ID,
- LabelID: label.ID,
- }); err != nil {
- return err
- }
-
- if err = issue.LoadRepo(ctx); err != nil {
- return
- }
-
- opts := &CreateCommentOptions{
- Type: CommentTypeLabel,
- Doer: doer,
- Repo: issue.Repo,
- Issue: issue,
- Label: label,
- Content: "1",
- }
- if _, err = CreateComment(ctx, opts); err != nil {
- return err
- }
-
- return updateLabelCols(ctx, label, "num_issues", "num_closed_issue")
-}
-
-// Remove all issue labels in the given exclusive scope
-func RemoveDuplicateExclusiveIssueLabels(ctx context.Context, issue *Issue, label *Label, doer *user_model.User) (err error) {
- scope := label.ExclusiveScope()
- if scope == "" {
- return nil
- }
-
- var toRemove []*Label
- for _, issueLabel := range issue.Labels {
- if label.ID != issueLabel.ID && issueLabel.ExclusiveScope() == scope {
- toRemove = append(toRemove, issueLabel)
- }
- }
-
- for _, issueLabel := range toRemove {
- if err = deleteIssueLabel(ctx, issue, issueLabel, doer); err != nil {
- return err
- }
- }
-
- return nil
-}
-
-// NewIssueLabel creates a new issue-label relation.
-func NewIssueLabel(issue *Issue, label *Label, doer *user_model.User) (err error) {
- if HasIssueLabel(db.DefaultContext, issue.ID, label.ID) {
- return nil
- }
-
- ctx, committer, err := db.TxContext(db.DefaultContext)
- if err != nil {
- return err
- }
- defer committer.Close()
-
- if err = issue.LoadRepo(ctx); err != nil {
- return err
- }
-
- // Do NOT add invalid labels
- if issue.RepoID != label.RepoID && issue.Repo.OwnerID != label.OrgID {
- return nil
- }
-
- if err = RemoveDuplicateExclusiveIssueLabels(ctx, issue, label, doer); err != nil {
- return nil
- }
-
- if err = newIssueLabel(ctx, issue, label, doer); err != nil {
- return err
- }
-
- issue.Labels = nil
- if err = issue.LoadLabels(ctx); err != nil {
- return err
- }
-
- return committer.Commit()
-}
-
-// newIssueLabels add labels to an issue. It will check if the labels are valid for the issue
-func newIssueLabels(ctx context.Context, issue *Issue, labels []*Label, doer *user_model.User) (err error) {
- if err = issue.LoadRepo(ctx); err != nil {
- return err
- }
- for _, l := range labels {
- // Don't add already present labels and invalid labels
- if HasIssueLabel(ctx, issue.ID, l.ID) ||
- (l.RepoID != issue.RepoID && l.OrgID != issue.Repo.OwnerID) {
- continue
- }
-
- if err = newIssueLabel(ctx, issue, l, doer); err != nil {
- return fmt.Errorf("newIssueLabel: %w", err)
- }
- }
-
- return nil
-}
-
-// NewIssueLabels creates a list of issue-label relations.
-func NewIssueLabels(issue *Issue, labels []*Label, doer *user_model.User) (err error) {
- ctx, committer, err := db.TxContext(db.DefaultContext)
- if err != nil {
- return err
- }
- defer committer.Close()
-
- if err = newIssueLabels(ctx, issue, labels, doer); err != nil {
- return err
- }
-
- issue.Labels = nil
- if err = issue.LoadLabels(ctx); err != nil {
- return err
- }
-
- return committer.Commit()
-}
-
-func deleteIssueLabel(ctx context.Context, issue *Issue, label *Label, doer *user_model.User) (err error) {
- if count, err := db.DeleteByBean(ctx, &IssueLabel{
- IssueID: issue.ID,
- LabelID: label.ID,
- }); err != nil {
- return err
- } else if count == 0 {
- return nil
- }
-
- if err = issue.LoadRepo(ctx); err != nil {
- return
- }
-
- opts := &CreateCommentOptions{
- Type: CommentTypeLabel,
- Doer: doer,
- Repo: issue.Repo,
- Issue: issue,
- Label: label,
- }
- if _, err = CreateComment(ctx, opts); err != nil {
- return err
- }
-
- return updateLabelCols(ctx, label, "num_issues", "num_closed_issue")
-}
-
-// DeleteIssueLabel deletes issue-label relation.
-func DeleteIssueLabel(ctx context.Context, issue *Issue, label *Label, doer *user_model.User) error {
- if err := deleteIssueLabel(ctx, issue, label, doer); err != nil {
- return err
- }
-
- issue.Labels = nil
- return issue.LoadLabels(ctx)
-}
-
-// DeleteLabelsByRepoID deletes labels of some repository
-func DeleteLabelsByRepoID(ctx context.Context, repoID int64) error {
- deleteCond := builder.Select("id").From("label").Where(builder.Eq{"label.repo_id": repoID})
-
- if _, err := db.GetEngine(ctx).In("label_id", deleteCond).
- Delete(&IssueLabel{}); err != nil {
- return err
- }
-
- _, err := db.DeleteByBean(ctx, &Label{RepoID: repoID})
- return err
-}
-
-// CountOrphanedLabels return count of labels witch are broken and not accessible via ui anymore
-func CountOrphanedLabels(ctx context.Context) (int64, error) {
- noref, err := db.GetEngine(ctx).Table("label").Where("repo_id=? AND org_id=?", 0, 0).Count()
- if err != nil {
- return 0, err
- }
-
- norepo, err := db.GetEngine(ctx).Table("label").
- Where(builder.And(
- builder.Gt{"repo_id": 0},
- builder.NotIn("repo_id", builder.Select("id").From("`repository`")),
- )).
- Count()
- if err != nil {
- return 0, err
- }
-
- noorg, err := db.GetEngine(ctx).Table("label").
- Where(builder.And(
- builder.Gt{"org_id": 0},
- builder.NotIn("org_id", builder.Select("id").From("`user`")),
- )).
- Count()
- if err != nil {
- return 0, err
- }
-
- return noref + norepo + noorg, nil
-}
-
-// DeleteOrphanedLabels delete labels witch are broken and not accessible via ui anymore
-func DeleteOrphanedLabels(ctx context.Context) error {
- // delete labels with no reference
- if _, err := db.GetEngine(ctx).Table("label").Where("repo_id=? AND org_id=?", 0, 0).Delete(new(Label)); err != nil {
- return err
- }
-
- // delete labels with none existing repos
- if _, err := db.GetEngine(ctx).
- Where(builder.And(
- builder.Gt{"repo_id": 0},
- builder.NotIn("repo_id", builder.Select("id").From("`repository`")),
- )).
- Delete(Label{}); err != nil {
- return err
- }
-
- // delete labels with none existing orgs
- if _, err := db.GetEngine(ctx).
- Where(builder.And(
- builder.Gt{"org_id": 0},
- builder.NotIn("org_id", builder.Select("id").From("`user`")),
- )).
- Delete(Label{}); err != nil {
- return err
- }
-
- return nil
-}
-
-// CountOrphanedIssueLabels return count of IssueLabels witch have no label behind anymore
-func CountOrphanedIssueLabels(ctx context.Context) (int64, error) {
- return db.GetEngine(ctx).Table("issue_label").
- NotIn("label_id", builder.Select("id").From("label")).
- Count()
-}
-
-// DeleteOrphanedIssueLabels delete IssueLabels witch have no label behind anymore
-func DeleteOrphanedIssueLabels(ctx context.Context) error {
- _, err := db.GetEngine(ctx).
- NotIn("label_id", builder.Select("id").From("label")).
- Delete(IssueLabel{})
- return err
-}
-
-// CountIssueLabelWithOutsideLabels count label comments with outside label
-func CountIssueLabelWithOutsideLabels(ctx context.Context) (int64, error) {
- return db.GetEngine(ctx).Where(builder.Expr("(label.org_id = 0 AND issue.repo_id != label.repo_id) OR (label.repo_id = 0 AND label.org_id != repository.owner_id)")).
- Table("issue_label").
- Join("inner", "label", "issue_label.label_id = label.id ").
- Join("inner", "issue", "issue.id = issue_label.issue_id ").
- Join("inner", "repository", "issue.repo_id = repository.id").
- Count(new(IssueLabel))
-}
-
-// FixIssueLabelWithOutsideLabels fix label comments with outside label
-func FixIssueLabelWithOutsideLabels(ctx context.Context) (int64, error) {
- res, err := db.GetEngine(ctx).Exec(`DELETE FROM issue_label WHERE issue_label.id IN (
- SELECT il_too.id FROM (
- SELECT il_too_too.id
- FROM issue_label AS il_too_too
- INNER JOIN label ON il_too_too.label_id = label.id
- INNER JOIN issue on issue.id = il_too_too.issue_id
- INNER JOIN repository on repository.id = issue.repo_id
- WHERE
- (label.org_id = 0 AND issue.repo_id != label.repo_id) OR (label.repo_id = 0 AND label.org_id != repository.owner_id)
- ) AS il_too )`)
- if err != nil {
- return 0, err
- }
-
- return res.RowsAffected()
-}