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 }