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 }