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

     1  // Copyright 2016 The Gogs Authors. All rights reserved.
     2  // Copyright 2020 The Gitea Authors.
     3  // SPDX-License-Identifier: MIT
     4  
     5  package issues
     6  
     7  import (
     8  	"context"
     9  	"fmt"
    10  	"strconv"
    11  	"strings"
    12  
    13  	"code.gitea.io/gitea/models/db"
    14  	"code.gitea.io/gitea/modules/label"
    15  	"code.gitea.io/gitea/modules/optional"
    16  	"code.gitea.io/gitea/modules/timeutil"
    17  	"code.gitea.io/gitea/modules/util"
    18  
    19  	"xorm.io/builder"
    20  )
    21  
    22  // ErrRepoLabelNotExist represents a "RepoLabelNotExist" kind of error.
    23  type ErrRepoLabelNotExist struct {
    24  	LabelID int64
    25  	RepoID  int64
    26  }
    27  
    28  // IsErrRepoLabelNotExist checks if an error is a RepoErrLabelNotExist.
    29  func IsErrRepoLabelNotExist(err error) bool {
    30  	_, ok := err.(ErrRepoLabelNotExist)
    31  	return ok
    32  }
    33  
    34  func (err ErrRepoLabelNotExist) Error() string {
    35  	return fmt.Sprintf("label does not exist [label_id: %d, repo_id: %d]", err.LabelID, err.RepoID)
    36  }
    37  
    38  func (err ErrRepoLabelNotExist) Unwrap() error {
    39  	return util.ErrNotExist
    40  }
    41  
    42  // ErrOrgLabelNotExist represents a "OrgLabelNotExist" kind of error.
    43  type ErrOrgLabelNotExist struct {
    44  	LabelID int64
    45  	OrgID   int64
    46  }
    47  
    48  // IsErrOrgLabelNotExist checks if an error is a OrgErrLabelNotExist.
    49  func IsErrOrgLabelNotExist(err error) bool {
    50  	_, ok := err.(ErrOrgLabelNotExist)
    51  	return ok
    52  }
    53  
    54  func (err ErrOrgLabelNotExist) Error() string {
    55  	return fmt.Sprintf("label does not exist [label_id: %d, org_id: %d]", err.LabelID, err.OrgID)
    56  }
    57  
    58  func (err ErrOrgLabelNotExist) Unwrap() error {
    59  	return util.ErrNotExist
    60  }
    61  
    62  // ErrLabelNotExist represents a "LabelNotExist" kind of error.
    63  type ErrLabelNotExist struct {
    64  	LabelID int64
    65  }
    66  
    67  // IsErrLabelNotExist checks if an error is a ErrLabelNotExist.
    68  func IsErrLabelNotExist(err error) bool {
    69  	_, ok := err.(ErrLabelNotExist)
    70  	return ok
    71  }
    72  
    73  func (err ErrLabelNotExist) Error() string {
    74  	return fmt.Sprintf("label does not exist [label_id: %d]", err.LabelID)
    75  }
    76  
    77  func (err ErrLabelNotExist) Unwrap() error {
    78  	return util.ErrNotExist
    79  }
    80  
    81  // Label represents a label of repository for issues.
    82  type Label struct {
    83  	ID              int64 `xorm:"pk autoincr"`
    84  	RepoID          int64 `xorm:"INDEX"`
    85  	OrgID           int64 `xorm:"INDEX"`
    86  	Name            string
    87  	Exclusive       bool
    88  	Description     string
    89  	Color           string `xorm:"VARCHAR(7)"`
    90  	NumIssues       int
    91  	NumClosedIssues int
    92  	CreatedUnix     timeutil.TimeStamp `xorm:"INDEX created"`
    93  	UpdatedUnix     timeutil.TimeStamp `xorm:"INDEX updated"`
    94  
    95  	NumOpenIssues     int    `xorm:"-"`
    96  	NumOpenRepoIssues int64  `xorm:"-"`
    97  	IsChecked         bool   `xorm:"-"`
    98  	QueryString       string `xorm:"-"`
    99  	IsSelected        bool   `xorm:"-"`
   100  	IsExcluded        bool   `xorm:"-"`
   101  
   102  	ArchivedUnix timeutil.TimeStamp `xorm:"DEFAULT NULL"`
   103  }
   104  
   105  func init() {
   106  	db.RegisterModel(new(Label))
   107  	db.RegisterModel(new(IssueLabel))
   108  }
   109  
   110  // CalOpenIssues sets the number of open issues of a label based on the already stored number of closed issues.
   111  func (l *Label) CalOpenIssues() {
   112  	l.NumOpenIssues = l.NumIssues - l.NumClosedIssues
   113  }
   114  
   115  // SetArchived set the label as archived
   116  func (l *Label) SetArchived(isArchived bool) {
   117  	if !isArchived {
   118  		l.ArchivedUnix = timeutil.TimeStamp(0)
   119  	} else if isArchived && !l.IsArchived() {
   120  		// Only change the date when it is newly archived.
   121  		l.ArchivedUnix = timeutil.TimeStampNow()
   122  	}
   123  }
   124  
   125  // IsArchived returns true if label is an archived
   126  func (l *Label) IsArchived() bool {
   127  	return !l.ArchivedUnix.IsZero()
   128  }
   129  
   130  // CalOpenOrgIssues calculates the open issues of a label for a specific repo
   131  func (l *Label) CalOpenOrgIssues(ctx context.Context, repoID, labelID int64) {
   132  	counts, _ := CountIssuesByRepo(ctx, &IssuesOptions{
   133  		RepoIDs:  []int64{repoID},
   134  		LabelIDs: []int64{labelID},
   135  		IsClosed: optional.Some(false),
   136  	})
   137  
   138  	for _, count := range counts {
   139  		l.NumOpenRepoIssues += count
   140  	}
   141  }
   142  
   143  // LoadSelectedLabelsAfterClick calculates the set of selected labels when a label is clicked
   144  func (l *Label) LoadSelectedLabelsAfterClick(currentSelectedLabels []int64, currentSelectedExclusiveScopes []string) {
   145  	var labelQuerySlice []string
   146  	labelSelected := false
   147  	labelID := strconv.FormatInt(l.ID, 10)
   148  	labelScope := l.ExclusiveScope()
   149  	for i, s := range currentSelectedLabels {
   150  		if s == l.ID {
   151  			labelSelected = true
   152  		} else if -s == l.ID {
   153  			labelSelected = true
   154  			l.IsExcluded = true
   155  		} else if s != 0 {
   156  			// Exclude other labels in the same scope from selection
   157  			if s < 0 || labelScope == "" || labelScope != currentSelectedExclusiveScopes[i] {
   158  				labelQuerySlice = append(labelQuerySlice, strconv.FormatInt(s, 10))
   159  			}
   160  		}
   161  	}
   162  	if !labelSelected {
   163  		labelQuerySlice = append(labelQuerySlice, labelID)
   164  	}
   165  	l.IsSelected = labelSelected
   166  	l.QueryString = strings.Join(labelQuerySlice, ",")
   167  }
   168  
   169  // BelongsToOrg returns true if label is an organization label
   170  func (l *Label) BelongsToOrg() bool {
   171  	return l.OrgID > 0
   172  }
   173  
   174  // BelongsToRepo returns true if label is a repository label
   175  func (l *Label) BelongsToRepo() bool {
   176  	return l.RepoID > 0
   177  }
   178  
   179  // Return scope substring of label name, or empty string if none exists
   180  func (l *Label) ExclusiveScope() string {
   181  	if !l.Exclusive {
   182  		return ""
   183  	}
   184  	lastIndex := strings.LastIndex(l.Name, "/")
   185  	if lastIndex == -1 || lastIndex == 0 || lastIndex == len(l.Name)-1 {
   186  		return ""
   187  	}
   188  	return l.Name[:lastIndex]
   189  }
   190  
   191  // NewLabel creates a new label
   192  func NewLabel(ctx context.Context, l *Label) error {
   193  	color, err := label.NormalizeColor(l.Color)
   194  	if err != nil {
   195  		return err
   196  	}
   197  	l.Color = color
   198  
   199  	return db.Insert(ctx, l)
   200  }
   201  
   202  // NewLabels creates new labels
   203  func NewLabels(ctx context.Context, labels ...*Label) error {
   204  	ctx, committer, err := db.TxContext(ctx)
   205  	if err != nil {
   206  		return err
   207  	}
   208  	defer committer.Close()
   209  
   210  	for _, l := range labels {
   211  		color, err := label.NormalizeColor(l.Color)
   212  		if err != nil {
   213  			return err
   214  		}
   215  		l.Color = color
   216  
   217  		if err := db.Insert(ctx, l); err != nil {
   218  			return err
   219  		}
   220  	}
   221  	return committer.Commit()
   222  }
   223  
   224  // UpdateLabel updates label information.
   225  func UpdateLabel(ctx context.Context, l *Label) error {
   226  	color, err := label.NormalizeColor(l.Color)
   227  	if err != nil {
   228  		return err
   229  	}
   230  	l.Color = color
   231  
   232  	return updateLabelCols(ctx, l, "name", "description", "color", "exclusive", "archived_unix")
   233  }
   234  
   235  // DeleteLabel delete a label
   236  func DeleteLabel(ctx context.Context, id, labelID int64) error {
   237  	l, err := GetLabelByID(ctx, labelID)
   238  	if err != nil {
   239  		if IsErrLabelNotExist(err) {
   240  			return nil
   241  		}
   242  		return err
   243  	}
   244  
   245  	ctx, committer, err := db.TxContext(ctx)
   246  	if err != nil {
   247  		return err
   248  	}
   249  	defer committer.Close()
   250  
   251  	sess := db.GetEngine(ctx)
   252  
   253  	if l.BelongsToOrg() && l.OrgID != id {
   254  		return nil
   255  	}
   256  	if l.BelongsToRepo() && l.RepoID != id {
   257  		return nil
   258  	}
   259  
   260  	if _, err = db.DeleteByID[Label](ctx, labelID); err != nil {
   261  		return err
   262  	} else if _, err = sess.
   263  		Where("label_id = ?", labelID).
   264  		Delete(new(IssueLabel)); err != nil {
   265  		return err
   266  	}
   267  
   268  	// delete comments about now deleted label_id
   269  	if _, err = sess.Where("label_id = ?", labelID).Cols("label_id").Delete(&Comment{}); err != nil {
   270  		return err
   271  	}
   272  
   273  	return committer.Commit()
   274  }
   275  
   276  // GetLabelByID returns a label by given ID.
   277  func GetLabelByID(ctx context.Context, labelID int64) (*Label, error) {
   278  	if labelID <= 0 {
   279  		return nil, ErrLabelNotExist{labelID}
   280  	}
   281  
   282  	l := &Label{}
   283  	has, err := db.GetEngine(ctx).ID(labelID).Get(l)
   284  	if err != nil {
   285  		return nil, err
   286  	} else if !has {
   287  		return nil, ErrLabelNotExist{l.ID}
   288  	}
   289  	return l, nil
   290  }
   291  
   292  // GetLabelsByIDs returns a list of labels by IDs
   293  func GetLabelsByIDs(ctx context.Context, labelIDs []int64, cols ...string) ([]*Label, error) {
   294  	labels := make([]*Label, 0, len(labelIDs))
   295  	return labels, db.GetEngine(ctx).Table("label").
   296  		In("id", labelIDs).
   297  		Asc("name").
   298  		Cols(cols...).
   299  		Find(&labels)
   300  }
   301  
   302  // GetLabelInRepoByName returns a label by name in given repository.
   303  func GetLabelInRepoByName(ctx context.Context, repoID int64, labelName string) (*Label, error) {
   304  	if len(labelName) == 0 || repoID <= 0 {
   305  		return nil, ErrRepoLabelNotExist{0, repoID}
   306  	}
   307  
   308  	l, exist, err := db.Get[Label](ctx, builder.Eq{"name": labelName, "repo_id": repoID})
   309  	if err != nil {
   310  		return nil, err
   311  	} else if !exist {
   312  		return nil, ErrRepoLabelNotExist{0, repoID}
   313  	}
   314  	return l, nil
   315  }
   316  
   317  // GetLabelInRepoByID returns a label by ID in given repository.
   318  func GetLabelInRepoByID(ctx context.Context, repoID, labelID int64) (*Label, error) {
   319  	if labelID <= 0 || repoID <= 0 {
   320  		return nil, ErrRepoLabelNotExist{labelID, repoID}
   321  	}
   322  
   323  	l, exist, err := db.Get[Label](ctx, builder.Eq{"id": labelID, "repo_id": repoID})
   324  	if err != nil {
   325  		return nil, err
   326  	} else if !exist {
   327  		return nil, ErrRepoLabelNotExist{labelID, repoID}
   328  	}
   329  	return l, nil
   330  }
   331  
   332  // GetLabelIDsInRepoByNames returns a list of labelIDs by names in a given
   333  // repository.
   334  // it silently ignores label names that do not belong to the repository.
   335  func GetLabelIDsInRepoByNames(ctx context.Context, repoID int64, labelNames []string) ([]int64, error) {
   336  	labelIDs := make([]int64, 0, len(labelNames))
   337  	return labelIDs, db.GetEngine(ctx).Table("label").
   338  		Where("repo_id = ?", repoID).
   339  		In("name", labelNames).
   340  		Asc("name").
   341  		Cols("id").
   342  		Find(&labelIDs)
   343  }
   344  
   345  // BuildLabelNamesIssueIDsCondition returns a builder where get issue ids match label names
   346  func BuildLabelNamesIssueIDsCondition(labelNames []string) *builder.Builder {
   347  	return builder.Select("issue_label.issue_id").
   348  		From("issue_label").
   349  		InnerJoin("label", "label.id = issue_label.label_id").
   350  		Where(
   351  			builder.In("label.name", labelNames),
   352  		).
   353  		GroupBy("issue_label.issue_id")
   354  }
   355  
   356  // GetLabelsInRepoByIDs returns a list of labels by IDs in given repository,
   357  // it silently ignores label IDs that do not belong to the repository.
   358  func GetLabelsInRepoByIDs(ctx context.Context, repoID int64, labelIDs []int64) ([]*Label, error) {
   359  	labels := make([]*Label, 0, len(labelIDs))
   360  	return labels, db.GetEngine(ctx).
   361  		Where("repo_id = ?", repoID).
   362  		In("id", labelIDs).
   363  		Asc("name").
   364  		Find(&labels)
   365  }
   366  
   367  // GetLabelsByRepoID returns all labels that belong to given repository by ID.
   368  func GetLabelsByRepoID(ctx context.Context, repoID int64, sortType string, listOptions db.ListOptions) ([]*Label, error) {
   369  	if repoID <= 0 {
   370  		return nil, ErrRepoLabelNotExist{0, repoID}
   371  	}
   372  	labels := make([]*Label, 0, 10)
   373  	sess := db.GetEngine(ctx).Where("repo_id = ?", repoID)
   374  
   375  	switch sortType {
   376  	case "reversealphabetically":
   377  		sess.Desc("name")
   378  	case "leastissues":
   379  		sess.Asc("num_issues")
   380  	case "mostissues":
   381  		sess.Desc("num_issues")
   382  	default:
   383  		sess.Asc("name")
   384  	}
   385  
   386  	if listOptions.Page != 0 {
   387  		sess = db.SetSessionPagination(sess, &listOptions)
   388  	}
   389  
   390  	return labels, sess.Find(&labels)
   391  }
   392  
   393  // CountLabelsByRepoID count number of all labels that belong to given repository by ID.
   394  func CountLabelsByRepoID(ctx context.Context, repoID int64) (int64, error) {
   395  	return db.GetEngine(ctx).Where("repo_id = ?", repoID).Count(&Label{})
   396  }
   397  
   398  // GetLabelInOrgByName returns a label by name in given organization.
   399  func GetLabelInOrgByName(ctx context.Context, orgID int64, labelName string) (*Label, error) {
   400  	if len(labelName) == 0 || orgID <= 0 {
   401  		return nil, ErrOrgLabelNotExist{0, orgID}
   402  	}
   403  
   404  	l, exist, err := db.Get[Label](ctx, builder.Eq{"name": labelName, "org_id": orgID})
   405  	if err != nil {
   406  		return nil, err
   407  	} else if !exist {
   408  		return nil, ErrOrgLabelNotExist{0, orgID}
   409  	}
   410  	return l, nil
   411  }
   412  
   413  // GetLabelInOrgByID returns a label by ID in given organization.
   414  func GetLabelInOrgByID(ctx context.Context, orgID, labelID int64) (*Label, error) {
   415  	if labelID <= 0 || orgID <= 0 {
   416  		return nil, ErrOrgLabelNotExist{labelID, orgID}
   417  	}
   418  
   419  	l, exist, err := db.Get[Label](ctx, builder.Eq{"id": labelID, "org_id": orgID})
   420  	if err != nil {
   421  		return nil, err
   422  	} else if !exist {
   423  		return nil, ErrOrgLabelNotExist{labelID, orgID}
   424  	}
   425  	return l, nil
   426  }
   427  
   428  // GetLabelsInOrgByIDs returns a list of labels by IDs in given organization,
   429  // it silently ignores label IDs that do not belong to the organization.
   430  func GetLabelsInOrgByIDs(ctx context.Context, orgID int64, labelIDs []int64) ([]*Label, error) {
   431  	labels := make([]*Label, 0, len(labelIDs))
   432  	return labels, db.GetEngine(ctx).
   433  		Where("org_id = ?", orgID).
   434  		In("id", labelIDs).
   435  		Asc("name").
   436  		Find(&labels)
   437  }
   438  
   439  // GetLabelsByOrgID returns all labels that belong to given organization by ID.
   440  func GetLabelsByOrgID(ctx context.Context, orgID int64, sortType string, listOptions db.ListOptions) ([]*Label, error) {
   441  	if orgID <= 0 {
   442  		return nil, ErrOrgLabelNotExist{0, orgID}
   443  	}
   444  	labels := make([]*Label, 0, 10)
   445  	sess := db.GetEngine(ctx).Where("org_id = ?", orgID)
   446  
   447  	switch sortType {
   448  	case "reversealphabetically":
   449  		sess.Desc("name")
   450  	case "leastissues":
   451  		sess.Asc("num_issues")
   452  	case "mostissues":
   453  		sess.Desc("num_issues")
   454  	default:
   455  		sess.Asc("name")
   456  	}
   457  
   458  	if listOptions.Page != 0 {
   459  		sess = db.SetSessionPagination(sess, &listOptions)
   460  	}
   461  
   462  	return labels, sess.Find(&labels)
   463  }
   464  
   465  // GetLabelIDsByNames returns a list of labelIDs by names.
   466  // It doesn't filter them by repo or org, so it could return labels belonging to different repos/orgs.
   467  // It's used for filtering issues via indexer, otherwise it would be useless.
   468  // Since it could return labels with the same name, so the length of returned ids could be more than the length of names.
   469  func GetLabelIDsByNames(ctx context.Context, labelNames []string) ([]int64, error) {
   470  	labelIDs := make([]int64, 0, len(labelNames))
   471  	return labelIDs, db.GetEngine(ctx).Table("label").
   472  		In("name", labelNames).
   473  		Cols("id").
   474  		Find(&labelIDs)
   475  }
   476  
   477  // CountLabelsByOrgID count all labels that belong to given organization by ID.
   478  func CountLabelsByOrgID(ctx context.Context, orgID int64) (int64, error) {
   479  	return db.GetEngine(ctx).Where("org_id = ?", orgID).Count(&Label{})
   480  }
   481  
   482  func updateLabelCols(ctx context.Context, l *Label, cols ...string) error {
   483  	_, err := db.GetEngine(ctx).ID(l.ID).
   484  		SetExpr("num_issues",
   485  			builder.Select("count(*)").From("issue_label").
   486  				Where(builder.Eq{"label_id": l.ID}),
   487  		).
   488  		SetExpr("num_closed_issues",
   489  			builder.Select("count(*)").From("issue_label").
   490  				InnerJoin("issue", "issue_label.issue_id = issue.id").
   491  				Where(builder.Eq{
   492  					"issue_label.label_id": l.ID,
   493  					"issue.is_closed":      true,
   494  				}),
   495  		).
   496  		Cols(cols...).Update(l)
   497  	return err
   498  }