code.gitea.io/gitea@v1.22.3/models/issues/issue_label.go (about)

     1  // Copyright 2023 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package issues
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  	"sort"
    10  
    11  	"code.gitea.io/gitea/models/db"
    12  	access_model "code.gitea.io/gitea/models/perm/access"
    13  	user_model "code.gitea.io/gitea/models/user"
    14  
    15  	"xorm.io/builder"
    16  )
    17  
    18  // IssueLabel represents an issue-label relation.
    19  type IssueLabel struct {
    20  	ID      int64 `xorm:"pk autoincr"`
    21  	IssueID int64 `xorm:"UNIQUE(s)"`
    22  	LabelID int64 `xorm:"UNIQUE(s)"`
    23  }
    24  
    25  // HasIssueLabel returns true if issue has been labeled.
    26  func HasIssueLabel(ctx context.Context, issueID, labelID int64) bool {
    27  	has, _ := db.GetEngine(ctx).Where("issue_id = ? AND label_id = ?", issueID, labelID).Get(new(IssueLabel))
    28  	return has
    29  }
    30  
    31  // newIssueLabel this function creates a new label it does not check if the label is valid for the issue
    32  // YOU MUST CHECK THIS BEFORE THIS FUNCTION
    33  func newIssueLabel(ctx context.Context, issue *Issue, label *Label, doer *user_model.User) (err error) {
    34  	if err = db.Insert(ctx, &IssueLabel{
    35  		IssueID: issue.ID,
    36  		LabelID: label.ID,
    37  	}); err != nil {
    38  		return err
    39  	}
    40  
    41  	if err = issue.LoadRepo(ctx); err != nil {
    42  		return err
    43  	}
    44  
    45  	opts := &CreateCommentOptions{
    46  		Type:    CommentTypeLabel,
    47  		Doer:    doer,
    48  		Repo:    issue.Repo,
    49  		Issue:   issue,
    50  		Label:   label,
    51  		Content: "1",
    52  	}
    53  	if _, err = CreateComment(ctx, opts); err != nil {
    54  		return err
    55  	}
    56  
    57  	issue.Labels = append(issue.Labels, label)
    58  
    59  	return updateLabelCols(ctx, label, "num_issues", "num_closed_issue")
    60  }
    61  
    62  // Remove all issue labels in the given exclusive scope
    63  func RemoveDuplicateExclusiveIssueLabels(ctx context.Context, issue *Issue, label *Label, doer *user_model.User) (err error) {
    64  	scope := label.ExclusiveScope()
    65  	if scope == "" {
    66  		return nil
    67  	}
    68  
    69  	var toRemove []*Label
    70  	for _, issueLabel := range issue.Labels {
    71  		if label.ID != issueLabel.ID && issueLabel.ExclusiveScope() == scope {
    72  			toRemove = append(toRemove, issueLabel)
    73  		}
    74  	}
    75  
    76  	for _, issueLabel := range toRemove {
    77  		if err = deleteIssueLabel(ctx, issue, issueLabel, doer); err != nil {
    78  			return err
    79  		}
    80  	}
    81  
    82  	return nil
    83  }
    84  
    85  // NewIssueLabel creates a new issue-label relation.
    86  func NewIssueLabel(ctx context.Context, issue *Issue, label *Label, doer *user_model.User) (err error) {
    87  	if HasIssueLabel(ctx, issue.ID, label.ID) {
    88  		return nil
    89  	}
    90  
    91  	ctx, committer, err := db.TxContext(ctx)
    92  	if err != nil {
    93  		return err
    94  	}
    95  	defer committer.Close()
    96  
    97  	if err = issue.LoadRepo(ctx); err != nil {
    98  		return err
    99  	}
   100  
   101  	// Do NOT add invalid labels
   102  	if issue.RepoID != label.RepoID && issue.Repo.OwnerID != label.OrgID {
   103  		return nil
   104  	}
   105  
   106  	if err = RemoveDuplicateExclusiveIssueLabels(ctx, issue, label, doer); err != nil {
   107  		return nil
   108  	}
   109  
   110  	if err = newIssueLabel(ctx, issue, label, doer); err != nil {
   111  		return err
   112  	}
   113  
   114  	issue.Labels = nil
   115  	if err = issue.LoadLabels(ctx); err != nil {
   116  		return err
   117  	}
   118  
   119  	return committer.Commit()
   120  }
   121  
   122  // newIssueLabels add labels to an issue. It will check if the labels are valid for the issue
   123  func newIssueLabels(ctx context.Context, issue *Issue, labels []*Label, doer *user_model.User) (err error) {
   124  	if err = issue.LoadRepo(ctx); err != nil {
   125  		return err
   126  	}
   127  
   128  	if err = issue.LoadLabels(ctx); err != nil {
   129  		return err
   130  	}
   131  
   132  	for _, l := range labels {
   133  		// Don't add already present labels and invalid labels
   134  		if HasIssueLabel(ctx, issue.ID, l.ID) ||
   135  			(l.RepoID != issue.RepoID && l.OrgID != issue.Repo.OwnerID) {
   136  			continue
   137  		}
   138  
   139  		if err = RemoveDuplicateExclusiveIssueLabels(ctx, issue, l, doer); err != nil {
   140  			return err
   141  		}
   142  
   143  		if err = newIssueLabel(ctx, issue, l, doer); err != nil {
   144  			return fmt.Errorf("newIssueLabel: %w", err)
   145  		}
   146  	}
   147  
   148  	return nil
   149  }
   150  
   151  // NewIssueLabels creates a list of issue-label relations.
   152  func NewIssueLabels(ctx context.Context, issue *Issue, labels []*Label, doer *user_model.User) (err error) {
   153  	ctx, committer, err := db.TxContext(ctx)
   154  	if err != nil {
   155  		return err
   156  	}
   157  	defer committer.Close()
   158  
   159  	if err = newIssueLabels(ctx, issue, labels, doer); err != nil {
   160  		return err
   161  	}
   162  
   163  	issue.Labels = nil
   164  	if err = issue.LoadLabels(ctx); err != nil {
   165  		return err
   166  	}
   167  
   168  	return committer.Commit()
   169  }
   170  
   171  func deleteIssueLabel(ctx context.Context, issue *Issue, label *Label, doer *user_model.User) (err error) {
   172  	if count, err := db.DeleteByBean(ctx, &IssueLabel{
   173  		IssueID: issue.ID,
   174  		LabelID: label.ID,
   175  	}); err != nil {
   176  		return err
   177  	} else if count == 0 {
   178  		return nil
   179  	}
   180  
   181  	if err = issue.LoadRepo(ctx); err != nil {
   182  		return err
   183  	}
   184  
   185  	opts := &CreateCommentOptions{
   186  		Type:  CommentTypeLabel,
   187  		Doer:  doer,
   188  		Repo:  issue.Repo,
   189  		Issue: issue,
   190  		Label: label,
   191  	}
   192  	if _, err = CreateComment(ctx, opts); err != nil {
   193  		return err
   194  	}
   195  
   196  	return updateLabelCols(ctx, label, "num_issues", "num_closed_issue")
   197  }
   198  
   199  // DeleteIssueLabel deletes issue-label relation.
   200  func DeleteIssueLabel(ctx context.Context, issue *Issue, label *Label, doer *user_model.User) error {
   201  	if err := deleteIssueLabel(ctx, issue, label, doer); err != nil {
   202  		return err
   203  	}
   204  
   205  	issue.Labels = nil
   206  	return issue.LoadLabels(ctx)
   207  }
   208  
   209  // DeleteLabelsByRepoID  deletes labels of some repository
   210  func DeleteLabelsByRepoID(ctx context.Context, repoID int64) error {
   211  	deleteCond := builder.Select("id").From("label").Where(builder.Eq{"label.repo_id": repoID})
   212  
   213  	if _, err := db.GetEngine(ctx).In("label_id", deleteCond).
   214  		Delete(&IssueLabel{}); err != nil {
   215  		return err
   216  	}
   217  
   218  	_, err := db.DeleteByBean(ctx, &Label{RepoID: repoID})
   219  	return err
   220  }
   221  
   222  // CountOrphanedLabels return count of labels witch are broken and not accessible via ui anymore
   223  func CountOrphanedLabels(ctx context.Context) (int64, error) {
   224  	noref, err := db.GetEngine(ctx).Table("label").Where("repo_id=? AND org_id=?", 0, 0).Count()
   225  	if err != nil {
   226  		return 0, err
   227  	}
   228  
   229  	norepo, err := db.GetEngine(ctx).Table("label").
   230  		Where(builder.And(
   231  			builder.Gt{"repo_id": 0},
   232  			builder.NotIn("repo_id", builder.Select("id").From("`repository`")),
   233  		)).
   234  		Count()
   235  	if err != nil {
   236  		return 0, err
   237  	}
   238  
   239  	noorg, err := db.GetEngine(ctx).Table("label").
   240  		Where(builder.And(
   241  			builder.Gt{"org_id": 0},
   242  			builder.NotIn("org_id", builder.Select("id").From("`user`")),
   243  		)).
   244  		Count()
   245  	if err != nil {
   246  		return 0, err
   247  	}
   248  
   249  	return noref + norepo + noorg, nil
   250  }
   251  
   252  // DeleteOrphanedLabels delete labels witch are broken and not accessible via ui anymore
   253  func DeleteOrphanedLabels(ctx context.Context) error {
   254  	// delete labels with no reference
   255  	if _, err := db.GetEngine(ctx).Table("label").Where("repo_id=? AND org_id=?", 0, 0).Delete(new(Label)); err != nil {
   256  		return err
   257  	}
   258  
   259  	// delete labels with none existing repos
   260  	if _, err := db.GetEngine(ctx).
   261  		Where(builder.And(
   262  			builder.Gt{"repo_id": 0},
   263  			builder.NotIn("repo_id", builder.Select("id").From("`repository`")),
   264  		)).
   265  		Delete(Label{}); err != nil {
   266  		return err
   267  	}
   268  
   269  	// delete labels with none existing orgs
   270  	if _, err := db.GetEngine(ctx).
   271  		Where(builder.And(
   272  			builder.Gt{"org_id": 0},
   273  			builder.NotIn("org_id", builder.Select("id").From("`user`")),
   274  		)).
   275  		Delete(Label{}); err != nil {
   276  		return err
   277  	}
   278  
   279  	return nil
   280  }
   281  
   282  // CountOrphanedIssueLabels return count of IssueLabels witch have no label behind anymore
   283  func CountOrphanedIssueLabels(ctx context.Context) (int64, error) {
   284  	return db.GetEngine(ctx).Table("issue_label").
   285  		NotIn("label_id", builder.Select("id").From("label")).
   286  		Count()
   287  }
   288  
   289  // DeleteOrphanedIssueLabels delete IssueLabels witch have no label behind anymore
   290  func DeleteOrphanedIssueLabels(ctx context.Context) error {
   291  	_, err := db.GetEngine(ctx).
   292  		NotIn("label_id", builder.Select("id").From("label")).
   293  		Delete(IssueLabel{})
   294  	return err
   295  }
   296  
   297  // CountIssueLabelWithOutsideLabels count label comments with outside label
   298  func CountIssueLabelWithOutsideLabels(ctx context.Context) (int64, error) {
   299  	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)")).
   300  		Table("issue_label").
   301  		Join("inner", "label", "issue_label.label_id = label.id ").
   302  		Join("inner", "issue", "issue.id = issue_label.issue_id ").
   303  		Join("inner", "repository", "issue.repo_id = repository.id").
   304  		Count(new(IssueLabel))
   305  }
   306  
   307  // FixIssueLabelWithOutsideLabels fix label comments with outside label
   308  func FixIssueLabelWithOutsideLabels(ctx context.Context) (int64, error) {
   309  	res, err := db.GetEngine(ctx).Exec(`DELETE FROM issue_label WHERE issue_label.id IN (
   310  		SELECT il_too.id FROM (
   311  			SELECT il_too_too.id
   312  				FROM issue_label AS il_too_too
   313  					INNER JOIN label ON il_too_too.label_id = label.id
   314  					INNER JOIN issue on issue.id = il_too_too.issue_id
   315  					INNER JOIN repository on repository.id = issue.repo_id
   316  				WHERE
   317  					(label.org_id = 0 AND issue.repo_id != label.repo_id) OR (label.repo_id = 0 AND label.org_id != repository.owner_id)
   318  	) AS il_too )`)
   319  	if err != nil {
   320  		return 0, err
   321  	}
   322  
   323  	return res.RowsAffected()
   324  }
   325  
   326  // LoadLabels loads labels
   327  func (issue *Issue) LoadLabels(ctx context.Context) (err error) {
   328  	if issue.Labels == nil && issue.ID != 0 {
   329  		issue.Labels, err = GetLabelsByIssueID(ctx, issue.ID)
   330  		if err != nil {
   331  			return fmt.Errorf("getLabelsByIssueID [%d]: %w", issue.ID, err)
   332  		}
   333  	}
   334  	return nil
   335  }
   336  
   337  // GetLabelsByIssueID returns all labels that belong to given issue by ID.
   338  func GetLabelsByIssueID(ctx context.Context, issueID int64) ([]*Label, error) {
   339  	var labels []*Label
   340  	return labels, db.GetEngine(ctx).Where("issue_label.issue_id = ?", issueID).
   341  		Join("LEFT", "issue_label", "issue_label.label_id = label.id").
   342  		Asc("label.name").
   343  		Find(&labels)
   344  }
   345  
   346  func clearIssueLabels(ctx context.Context, issue *Issue, doer *user_model.User) (err error) {
   347  	if err = issue.LoadLabels(ctx); err != nil {
   348  		return fmt.Errorf("getLabels: %w", err)
   349  	}
   350  
   351  	for i := range issue.Labels {
   352  		if err = deleteIssueLabel(ctx, issue, issue.Labels[i], doer); err != nil {
   353  			return fmt.Errorf("removeLabel: %w", err)
   354  		}
   355  	}
   356  
   357  	return nil
   358  }
   359  
   360  // ClearIssueLabels removes all issue labels as the given user.
   361  // Triggers appropriate WebHooks, if any.
   362  func ClearIssueLabels(ctx context.Context, issue *Issue, doer *user_model.User) (err error) {
   363  	ctx, committer, err := db.TxContext(ctx)
   364  	if err != nil {
   365  		return err
   366  	}
   367  	defer committer.Close()
   368  
   369  	if err := issue.LoadRepo(ctx); err != nil {
   370  		return err
   371  	} else if err = issue.LoadPullRequest(ctx); err != nil {
   372  		return err
   373  	}
   374  
   375  	perm, err := access_model.GetUserRepoPermission(ctx, issue.Repo, doer)
   376  	if err != nil {
   377  		return err
   378  	}
   379  	if !perm.CanWriteIssuesOrPulls(issue.IsPull) {
   380  		return ErrRepoLabelNotExist{}
   381  	}
   382  
   383  	if err = clearIssueLabels(ctx, issue, doer); err != nil {
   384  		return err
   385  	}
   386  
   387  	if err = committer.Commit(); err != nil {
   388  		return fmt.Errorf("Commit: %w", err)
   389  	}
   390  
   391  	return nil
   392  }
   393  
   394  type labelSorter []*Label
   395  
   396  func (ts labelSorter) Len() int {
   397  	return len([]*Label(ts))
   398  }
   399  
   400  func (ts labelSorter) Less(i, j int) bool {
   401  	return []*Label(ts)[i].ID < []*Label(ts)[j].ID
   402  }
   403  
   404  func (ts labelSorter) Swap(i, j int) {
   405  	[]*Label(ts)[i], []*Label(ts)[j] = []*Label(ts)[j], []*Label(ts)[i]
   406  }
   407  
   408  // Ensure only one label of a given scope exists, with labels at the end of the
   409  // array getting preference over earlier ones.
   410  func RemoveDuplicateExclusiveLabels(labels []*Label) []*Label {
   411  	validLabels := make([]*Label, 0, len(labels))
   412  
   413  	for i, label := range labels {
   414  		scope := label.ExclusiveScope()
   415  		if scope != "" {
   416  			foundOther := false
   417  			for _, otherLabel := range labels[i+1:] {
   418  				if otherLabel.ExclusiveScope() == scope {
   419  					foundOther = true
   420  					break
   421  				}
   422  			}
   423  			if foundOther {
   424  				continue
   425  			}
   426  		}
   427  		validLabels = append(validLabels, label)
   428  	}
   429  
   430  	return validLabels
   431  }
   432  
   433  // ReplaceIssueLabels removes all current labels and add new labels to the issue.
   434  // Triggers appropriate WebHooks, if any.
   435  func ReplaceIssueLabels(ctx context.Context, issue *Issue, labels []*Label, doer *user_model.User) (err error) {
   436  	ctx, committer, err := db.TxContext(ctx)
   437  	if err != nil {
   438  		return err
   439  	}
   440  	defer committer.Close()
   441  
   442  	if err = issue.LoadRepo(ctx); err != nil {
   443  		return err
   444  	}
   445  
   446  	if err = issue.LoadLabels(ctx); err != nil {
   447  		return err
   448  	}
   449  
   450  	labels = RemoveDuplicateExclusiveLabels(labels)
   451  
   452  	sort.Sort(labelSorter(labels))
   453  	sort.Sort(labelSorter(issue.Labels))
   454  
   455  	var toAdd, toRemove []*Label
   456  
   457  	addIndex, removeIndex := 0, 0
   458  	for addIndex < len(labels) && removeIndex < len(issue.Labels) {
   459  		addLabel := labels[addIndex]
   460  		removeLabel := issue.Labels[removeIndex]
   461  		if addLabel.ID == removeLabel.ID {
   462  			// Silently drop invalid labels
   463  			if removeLabel.RepoID != issue.RepoID && removeLabel.OrgID != issue.Repo.OwnerID {
   464  				toRemove = append(toRemove, removeLabel)
   465  			}
   466  
   467  			addIndex++
   468  			removeIndex++
   469  		} else if addLabel.ID < removeLabel.ID {
   470  			// Only add if the label is valid
   471  			if addLabel.RepoID == issue.RepoID || addLabel.OrgID == issue.Repo.OwnerID {
   472  				toAdd = append(toAdd, addLabel)
   473  			}
   474  			addIndex++
   475  		} else {
   476  			toRemove = append(toRemove, removeLabel)
   477  			removeIndex++
   478  		}
   479  	}
   480  	toAdd = append(toAdd, labels[addIndex:]...)
   481  	toRemove = append(toRemove, issue.Labels[removeIndex:]...)
   482  
   483  	if len(toAdd) > 0 {
   484  		if err = newIssueLabels(ctx, issue, toAdd, doer); err != nil {
   485  			return fmt.Errorf("addLabels: %w", err)
   486  		}
   487  	}
   488  
   489  	for _, l := range toRemove {
   490  		if err = deleteIssueLabel(ctx, issue, l, doer); err != nil {
   491  			return fmt.Errorf("removeLabel: %w", err)
   492  		}
   493  	}
   494  
   495  	issue.Labels = nil
   496  	if err = issue.LoadLabels(ctx); err != nil {
   497  		return err
   498  	}
   499  
   500  	return committer.Commit()
   501  }