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