507 lines · 13.2 KB · Go
RawDownload
// Copyright 2023 The Gitea Authors. All rights reserved.// SPDX-License-Identifier: MIT package issues import (	"context"	"fmt"	"sort" 	"forgejo.org/models/db"	access_model "forgejo.org/models/perm/access"	user_model "forgejo.org/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 FUNCTIONfunc 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 scopefunc 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	} 	ctx, committer, err := db.TxContext(ctx)	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	} 	if err = issue.ReloadLabels(ctx); err != nil {		return err	} 	return committer.Commit()} // newIssueLabels add labels to an issue. It will check if the labels are valid for the issuefunc 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) {	ctx, committer, err := db.TxContext(ctx)	if err != nil {		return err	}	defer committer.Close() 	if err = newIssueLabels(ctx, issue, labels, doer); err != nil {		return err	} 	if err = issue.ReloadLabels(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 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	} 	return issue.ReloadLabels(ctx)} // DeleteLabelsByRepoID  deletes labels of some repositoryfunc 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 anymorefunc 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 anymorefunc 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 anymorefunc 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 anymorefunc 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 labelfunc 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 labelfunc 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 only if they are not already setfunc (issue *Issue) LoadLabels(ctx context.Context) (err error) {	if !issue.isLabelsLoaded && issue.Labels == nil {		if err := issue.ReloadLabels(ctx); err != nil {			return err		}		issue.isLabelsLoaded = true	}	return nil} func (issue *Issue) ReloadLabels(ctx context.Context) (err error) {	if 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(ctx context.Context, issue *Issue, doer *user_model.User) (err error) {	ctx, committer, err := db.TxContext(ctx)	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(ctx context.Context, issue *Issue, labels []*Label, doer *user_model.User) (err error) {	ctx, committer, err := db.TxContext(ctx)	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)		}	}

Lines 1–500 of 507.