mirror of
https://github.com/go-gitea/gitea.git
synced 2026-10-20 08:20:04 +00:00
Try to figure out whether we can reuse the "comment" table for #36862 The legacy "CommitID" field is removed, and updated the comment. Some useless and outdated "doctor fixes" are also removed together.
442 lines
12 KiB
Go
442 lines
12 KiB
Go
// Copyright 2023 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package issues
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sort"
|
|
|
|
"gitea.dev/models/db"
|
|
access_model "gitea.dev/models/perm/access"
|
|
user_model "gitea.dev/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 err
|
|
}
|
|
|
|
opts := &CreateCommentOptions{
|
|
Type: CommentTypeLabel,
|
|
Doer: doer,
|
|
Repo: issue.Repo,
|
|
Issue: issue,
|
|
Label: label,
|
|
Content: "1",
|
|
}
|
|
if _, err = CreateComment(ctx, opts); err != nil {
|
|
return err
|
|
}
|
|
|
|
issue.Labels = append(issue.Labels, label)
|
|
|
|
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(ctx context.Context, issue *Issue, label *Label, doer *user_model.User) (err error) {
|
|
if HasIssueLabel(ctx, issue.ID, label.ID) {
|
|
return nil
|
|
}
|
|
|
|
return db.WithTx(ctx, func(ctx context.Context) error {
|
|
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.isLabelsLoaded = false
|
|
issue.Labels = nil
|
|
return issue.LoadLabels(ctx)
|
|
})
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
if err = issue.LoadLabels(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 = RemoveDuplicateExclusiveIssueLabels(ctx, issue, l, doer); err != nil {
|
|
return err
|
|
}
|
|
|
|
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(ctx context.Context, issue *Issue, labels []*Label, doer *user_model.User) (err error) {
|
|
return db.WithTx(ctx, func(ctx context.Context) error {
|
|
if err = newIssueLabels(ctx, issue, labels, doer); err != nil {
|
|
return err
|
|
}
|
|
|
|
// reload all labels
|
|
issue.isLabelsLoaded = false
|
|
issue.Labels = nil
|
|
return issue.LoadLabels(ctx)
|
|
})
|
|
}
|
|
|
|
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 err
|
|
}
|
|
|
|
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
|
|
issue.isLabelsLoaded = false
|
|
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
|
|
}
|
|
|
|
// LoadLabels loads labels
|
|
func (issue *Issue) LoadLabels(ctx context.Context) (err error) {
|
|
if !issue.isLabelsLoaded && 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)
|
|
}
|
|
issue.isLabelsLoaded = true
|
|
}
|
|
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(ctx context.Context, issue *Issue, doer *user_model.User) (err error) {
|
|
return db.WithTx(ctx, func(ctx context.Context) error {
|
|
if err := issue.LoadRepo(ctx); err != nil {
|
|
return err
|
|
} else if err = issue.LoadPullRequest(ctx); err != nil {
|
|
return err
|
|
}
|
|
|
|
perm, err := access_model.GetDoerRepoPermission(ctx, issue.Repo, doer)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !perm.CanWriteIssuesOrPulls(issue.IsPull) {
|
|
return ErrRepoLabelNotExist{}
|
|
}
|
|
|
|
return clearIssueLabels(ctx, issue, doer)
|
|
})
|
|
}
|
|
|
|
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(ctx context.Context, issue *Issue, labels []*Label, doer *user_model.User) (err error) {
|
|
return db.WithTx(ctx, func(ctx context.Context) error {
|
|
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
|
|
return issue.LoadLabels(ctx)
|
|
})
|
|
}
|