1// Copyright 2016 The Gogs Authors. All rights reserved. 2// Copyright 2020 The Gitea Authors. 3// Use of this source code is governed by a MIT-style 4// license that can be found in the LICENSE file. 5 6package models 7 8import ( 9 "context" 10 "fmt" 11 "html/template" 12 "math" 13 "regexp" 14 "strconv" 15 "strings" 16 17 "code.gitea.io/gitea/models/db" 18 user_model "code.gitea.io/gitea/models/user" 19 "code.gitea.io/gitea/modules/timeutil" 20 21 "xorm.io/builder" 22) 23 24// LabelColorPattern is a regexp witch can validate LabelColor 25var LabelColorPattern = regexp.MustCompile("^#[0-9a-fA-F]{6}$") 26 27// Label represents a label of repository for issues. 28type Label struct { 29 ID int64 `xorm:"pk autoincr"` 30 RepoID int64 `xorm:"INDEX"` 31 OrgID int64 `xorm:"INDEX"` 32 Name string 33 Description string 34 Color string `xorm:"VARCHAR(7)"` 35 NumIssues int 36 NumClosedIssues int 37 CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` 38 UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` 39 40 NumOpenIssues int `xorm:"-"` 41 NumOpenRepoIssues int64 `xorm:"-"` 42 IsChecked bool `xorm:"-"` 43 QueryString string `xorm:"-"` 44 IsSelected bool `xorm:"-"` 45 IsExcluded bool `xorm:"-"` 46} 47 48func init() { 49 db.RegisterModel(new(Label)) 50 db.RegisterModel(new(IssueLabel)) 51} 52 53// GetLabelTemplateFile loads the label template file by given name, 54// then parses and returns a list of name-color pairs and optionally description. 55func GetLabelTemplateFile(name string) ([][3]string, error) { 56 data, err := GetRepoInitFile("label", name) 57 if err != nil { 58 return nil, ErrIssueLabelTemplateLoad{name, fmt.Errorf("GetRepoInitFile: %v", err)} 59 } 60 61 lines := strings.Split(string(data), "\n") 62 list := make([][3]string, 0, len(lines)) 63 for i := 0; i < len(lines); i++ { 64 line := strings.TrimSpace(lines[i]) 65 if len(line) == 0 { 66 continue 67 } 68 69 parts := strings.SplitN(line, ";", 2) 70 71 fields := strings.SplitN(parts[0], " ", 2) 72 if len(fields) != 2 { 73 return nil, ErrIssueLabelTemplateLoad{name, fmt.Errorf("line is malformed: %s", line)} 74 } 75 76 color := strings.Trim(fields[0], " ") 77 if len(color) == 6 { 78 color = "#" + color 79 } 80 if !LabelColorPattern.MatchString(color) { 81 return nil, ErrIssueLabelTemplateLoad{name, fmt.Errorf("bad HTML color code in line: %s", line)} 82 } 83 84 var description string 85 86 if len(parts) > 1 { 87 description = strings.TrimSpace(parts[1]) 88 } 89 90 fields[1] = strings.TrimSpace(fields[1]) 91 list = append(list, [3]string{fields[1], color, description}) 92 } 93 94 return list, nil 95} 96 97// CalOpenIssues sets the number of open issues of a label based on the already stored number of closed issues. 98func (label *Label) CalOpenIssues() { 99 label.NumOpenIssues = label.NumIssues - label.NumClosedIssues 100} 101 102// CalOpenOrgIssues calculates the open issues of a label for a specific repo 103func (label *Label) CalOpenOrgIssues(repoID, labelID int64) { 104 repoIDs := []int64{repoID} 105 labelIDs := []int64{labelID} 106 107 counts, _ := CountIssuesByRepo(&IssuesOptions{ 108 RepoIDs: repoIDs, 109 LabelIDs: labelIDs, 110 }) 111 112 for _, count := range counts { 113 label.NumOpenRepoIssues += count 114 } 115} 116 117// LoadSelectedLabelsAfterClick calculates the set of selected labels when a label is clicked 118func (label *Label) LoadSelectedLabelsAfterClick(currentSelectedLabels []int64) { 119 var labelQuerySlice []string 120 labelSelected := false 121 labelID := strconv.FormatInt(label.ID, 10) 122 for _, s := range currentSelectedLabels { 123 if s == label.ID { 124 labelSelected = true 125 } else if -s == label.ID { 126 labelSelected = true 127 label.IsExcluded = true 128 } else if s != 0 { 129 labelQuerySlice = append(labelQuerySlice, strconv.FormatInt(s, 10)) 130 } 131 } 132 if !labelSelected { 133 labelQuerySlice = append(labelQuerySlice, labelID) 134 } 135 label.IsSelected = labelSelected 136 label.QueryString = strings.Join(labelQuerySlice, ",") 137} 138 139// BelongsToOrg returns true if label is an organization label 140func (label *Label) BelongsToOrg() bool { 141 return label.OrgID > 0 142} 143 144// BelongsToRepo returns true if label is a repository label 145func (label *Label) BelongsToRepo() bool { 146 return label.RepoID > 0 147} 148 149// SrgbToLinear converts a component of an sRGB color to its linear intensity 150// See: https://en.wikipedia.org/wiki/SRGB#The_reverse_transformation_(sRGB_to_CIE_XYZ) 151func SrgbToLinear(color uint8) float64 { 152 flt := float64(color) / 255 153 if flt <= 0.04045 { 154 return flt / 12.92 155 } 156 return math.Pow((flt+0.055)/1.055, 2.4) 157} 158 159// Luminance returns the luminance of an sRGB color 160func Luminance(color uint32) float64 { 161 r := SrgbToLinear(uint8(0xFF & (color >> 16))) 162 g := SrgbToLinear(uint8(0xFF & (color >> 8))) 163 b := SrgbToLinear(uint8(0xFF & color)) 164 165 // luminance ratios for sRGB 166 return 0.2126*r + 0.7152*g + 0.0722*b 167} 168 169// LuminanceThreshold is the luminance at which white and black appear to have the same contrast 170// i.e. x such that 1.05 / (x + 0.05) = (x + 0.05) / 0.05 171// i.e. math.Sqrt(1.05*0.05) - 0.05 172const LuminanceThreshold float64 = 0.179 173 174// ForegroundColor calculates the text color for labels based 175// on their background color. 176func (label *Label) ForegroundColor() template.CSS { 177 if strings.HasPrefix(label.Color, "#") { 178 if color, err := strconv.ParseUint(label.Color[1:], 16, 64); err == nil { 179 // NOTE: see web_src/js/components/ContextPopup.vue for similar implementation 180 luminance := Luminance(uint32(color)) 181 182 // prefer white or black based upon contrast 183 if luminance < LuminanceThreshold { 184 return template.CSS("#fff") 185 } 186 return template.CSS("#000") 187 } 188 } 189 190 // default to black 191 return template.CSS("#000") 192} 193 194// .____ ___. .__ 195// | | _____ \_ |__ ____ | | 196// | | \__ \ | __ \_/ __ \| | 197// | |___ / __ \| \_\ \ ___/| |__ 198// >_______ (____ /___ /\___ >____/ 199 200func loadLabels(labelTemplate string) ([]string, error) { 201 list, err := GetLabelTemplateFile(labelTemplate) 202 if err != nil { 203 return nil, err 204 } 205 206 labels := make([]string, len(list)) 207 for i := 0; i < len(list); i++ { 208 labels[i] = list[i][0] 209 } 210 return labels, nil 211} 212 213// LoadLabelsFormatted loads the labels' list of a template file as a string separated by comma 214func LoadLabelsFormatted(labelTemplate string) (string, error) { 215 labels, err := loadLabels(labelTemplate) 216 return strings.Join(labels, ", "), err 217} 218 219func initializeLabels(e db.Engine, id int64, labelTemplate string, isOrg bool) error { 220 list, err := GetLabelTemplateFile(labelTemplate) 221 if err != nil { 222 return err 223 } 224 225 labels := make([]*Label, len(list)) 226 for i := 0; i < len(list); i++ { 227 labels[i] = &Label{ 228 Name: list[i][0], 229 Description: list[i][2], 230 Color: list[i][1], 231 } 232 if isOrg { 233 labels[i].OrgID = id 234 } else { 235 labels[i].RepoID = id 236 } 237 } 238 for _, label := range labels { 239 if err = newLabel(e, label); err != nil { 240 return err 241 } 242 } 243 return nil 244} 245 246// InitializeLabels adds a label set to a repository using a template 247func InitializeLabels(ctx context.Context, repoID int64, labelTemplate string, isOrg bool) error { 248 return initializeLabels(db.GetEngine(ctx), repoID, labelTemplate, isOrg) 249} 250 251func newLabel(e db.Engine, label *Label) error { 252 _, err := e.Insert(label) 253 return err 254} 255 256// NewLabel creates a new label 257func NewLabel(label *Label) error { 258 if !LabelColorPattern.MatchString(label.Color) { 259 return fmt.Errorf("bad color code: %s", label.Color) 260 } 261 return newLabel(db.GetEngine(db.DefaultContext), label) 262} 263 264// NewLabels creates new labels 265func NewLabels(labels ...*Label) error { 266 ctx, committer, err := db.TxContext() 267 if err != nil { 268 return err 269 } 270 defer committer.Close() 271 272 for _, label := range labels { 273 if !LabelColorPattern.MatchString(label.Color) { 274 return fmt.Errorf("bad color code: %s", label.Color) 275 } 276 if err := newLabel(db.GetEngine(ctx), label); err != nil { 277 return err 278 } 279 } 280 return committer.Commit() 281} 282 283// UpdateLabel updates label information. 284func UpdateLabel(l *Label) error { 285 if !LabelColorPattern.MatchString(l.Color) { 286 return fmt.Errorf("bad color code: %s", l.Color) 287 } 288 return updateLabelCols(db.GetEngine(db.DefaultContext), l, "name", "description", "color") 289} 290 291// DeleteLabel delete a label 292func DeleteLabel(id, labelID int64) error { 293 label, err := GetLabelByID(labelID) 294 if err != nil { 295 if IsErrLabelNotExist(err) { 296 return nil 297 } 298 return err 299 } 300 301 ctx, committer, err := db.TxContext() 302 if err != nil { 303 return err 304 } 305 defer committer.Close() 306 307 sess := db.GetEngine(ctx) 308 309 if label.BelongsToOrg() && label.OrgID != id { 310 return nil 311 } 312 if label.BelongsToRepo() && label.RepoID != id { 313 return nil 314 } 315 316 if _, err = sess.ID(labelID).Delete(new(Label)); err != nil { 317 return err 318 } else if _, err = sess. 319 Where("label_id = ?", labelID). 320 Delete(new(IssueLabel)); err != nil { 321 return err 322 } 323 324 // delete comments about now deleted label_id 325 if _, err = sess.Where("label_id = ?", labelID).Cols("label_id").Delete(&Comment{}); err != nil { 326 return err 327 } 328 329 return committer.Commit() 330} 331 332// getLabelByID returns a label by label id 333func getLabelByID(e db.Engine, labelID int64) (*Label, error) { 334 if labelID <= 0 { 335 return nil, ErrLabelNotExist{labelID} 336 } 337 338 l := &Label{} 339 has, err := e.ID(labelID).Get(l) 340 if err != nil { 341 return nil, err 342 } else if !has { 343 return nil, ErrLabelNotExist{l.ID} 344 } 345 return l, nil 346} 347 348// GetLabelByID returns a label by given ID. 349func GetLabelByID(id int64) (*Label, error) { 350 return getLabelByID(db.GetEngine(db.DefaultContext), id) 351} 352 353// GetLabelsByIDs returns a list of labels by IDs 354func GetLabelsByIDs(labelIDs []int64) ([]*Label, error) { 355 labels := make([]*Label, 0, len(labelIDs)) 356 return labels, db.GetEngine(db.DefaultContext).Table("label"). 357 In("id", labelIDs). 358 Asc("name"). 359 Cols("id", "repo_id", "org_id"). 360 Find(&labels) 361} 362 363// __________ .__ __ 364// \______ \ ____ ______ ____ _____|__|/ |_ ___________ ___.__. 365// | _// __ \\____ \ / _ \/ ___/ \ __\/ _ \_ __ < | | 366// | | \ ___/| |_> > <_> )___ \| || | ( <_> ) | \/\___ | 367// |____|_ /\___ > __/ \____/____ >__||__| \____/|__| / ____| 368// \/ \/|__| \/ \/ 369 370// getLabelInRepoByName returns a label by Name in given repository. 371func getLabelInRepoByName(e db.Engine, repoID int64, labelName string) (*Label, error) { 372 if len(labelName) == 0 || repoID <= 0 { 373 return nil, ErrRepoLabelNotExist{0, repoID} 374 } 375 376 l := &Label{ 377 Name: labelName, 378 RepoID: repoID, 379 } 380 has, err := e.Get(l) 381 if err != nil { 382 return nil, err 383 } else if !has { 384 return nil, ErrRepoLabelNotExist{0, l.RepoID} 385 } 386 return l, nil 387} 388 389// getLabelInRepoByID returns a label by ID in given repository. 390func getLabelInRepoByID(e db.Engine, repoID, labelID int64) (*Label, error) { 391 if labelID <= 0 || repoID <= 0 { 392 return nil, ErrRepoLabelNotExist{labelID, repoID} 393 } 394 395 l := &Label{ 396 ID: labelID, 397 RepoID: repoID, 398 } 399 has, err := e.Get(l) 400 if err != nil { 401 return nil, err 402 } else if !has { 403 return nil, ErrRepoLabelNotExist{l.ID, l.RepoID} 404 } 405 return l, nil 406} 407 408// GetLabelInRepoByName returns a label by name in given repository. 409func GetLabelInRepoByName(repoID int64, labelName string) (*Label, error) { 410 return getLabelInRepoByName(db.GetEngine(db.DefaultContext), repoID, labelName) 411} 412 413// GetLabelIDsInRepoByNames returns a list of labelIDs by names in a given 414// repository. 415// it silently ignores label names that do not belong to the repository. 416func GetLabelIDsInRepoByNames(repoID int64, labelNames []string) ([]int64, error) { 417 labelIDs := make([]int64, 0, len(labelNames)) 418 return labelIDs, db.GetEngine(db.DefaultContext).Table("label"). 419 Where("repo_id = ?", repoID). 420 In("name", labelNames). 421 Asc("name"). 422 Cols("id"). 423 Find(&labelIDs) 424} 425 426// BuildLabelNamesIssueIDsCondition returns a builder where get issue ids match label names 427func BuildLabelNamesIssueIDsCondition(labelNames []string) *builder.Builder { 428 return builder.Select("issue_label.issue_id"). 429 From("issue_label"). 430 InnerJoin("label", "label.id = issue_label.label_id"). 431 Where( 432 builder.In("label.name", labelNames), 433 ). 434 GroupBy("issue_label.issue_id") 435} 436 437// GetLabelInRepoByID returns a label by ID in given repository. 438func GetLabelInRepoByID(repoID, labelID int64) (*Label, error) { 439 return getLabelInRepoByID(db.GetEngine(db.DefaultContext), repoID, labelID) 440} 441 442// GetLabelsInRepoByIDs returns a list of labels by IDs in given repository, 443// it silently ignores label IDs that do not belong to the repository. 444func GetLabelsInRepoByIDs(repoID int64, labelIDs []int64) ([]*Label, error) { 445 labels := make([]*Label, 0, len(labelIDs)) 446 return labels, db.GetEngine(db.DefaultContext). 447 Where("repo_id = ?", repoID). 448 In("id", labelIDs). 449 Asc("name"). 450 Find(&labels) 451} 452 453func getLabelsByRepoID(e db.Engine, repoID int64, sortType string, listOptions db.ListOptions) ([]*Label, error) { 454 if repoID <= 0 { 455 return nil, ErrRepoLabelNotExist{0, repoID} 456 } 457 labels := make([]*Label, 0, 10) 458 sess := e.Where("repo_id = ?", repoID) 459 460 switch sortType { 461 case "reversealphabetically": 462 sess.Desc("name") 463 case "leastissues": 464 sess.Asc("num_issues") 465 case "mostissues": 466 sess.Desc("num_issues") 467 default: 468 sess.Asc("name") 469 } 470 471 if listOptions.Page != 0 { 472 sess = db.SetSessionPagination(sess, &listOptions) 473 } 474 475 return labels, sess.Find(&labels) 476} 477 478// GetLabelsByRepoID returns all labels that belong to given repository by ID. 479func GetLabelsByRepoID(repoID int64, sortType string, listOptions db.ListOptions) ([]*Label, error) { 480 return getLabelsByRepoID(db.GetEngine(db.DefaultContext), repoID, sortType, listOptions) 481} 482 483// CountLabelsByRepoID count number of all labels that belong to given repository by ID. 484func CountLabelsByRepoID(repoID int64) (int64, error) { 485 return db.GetEngine(db.DefaultContext).Where("repo_id = ?", repoID).Count(&Label{}) 486} 487 488// ________ 489// \_____ \_______ ____ 490// / | \_ __ \/ ___\ 491// / | \ | \/ /_/ > 492// \_______ /__| \___ / 493// \/ /_____/ 494 495// getLabelInOrgByName returns a label by Name in given organization 496func getLabelInOrgByName(e db.Engine, orgID int64, labelName string) (*Label, error) { 497 if len(labelName) == 0 || orgID <= 0 { 498 return nil, ErrOrgLabelNotExist{0, orgID} 499 } 500 501 l := &Label{ 502 Name: labelName, 503 OrgID: orgID, 504 } 505 has, err := e.Get(l) 506 if err != nil { 507 return nil, err 508 } else if !has { 509 return nil, ErrOrgLabelNotExist{0, l.OrgID} 510 } 511 return l, nil 512} 513 514// getLabelInOrgByID returns a label by ID in given organization. 515func getLabelInOrgByID(e db.Engine, orgID, labelID int64) (*Label, error) { 516 if labelID <= 0 || orgID <= 0 { 517 return nil, ErrOrgLabelNotExist{labelID, orgID} 518 } 519 520 l := &Label{ 521 ID: labelID, 522 OrgID: orgID, 523 } 524 has, err := e.Get(l) 525 if err != nil { 526 return nil, err 527 } else if !has { 528 return nil, ErrOrgLabelNotExist{l.ID, l.OrgID} 529 } 530 return l, nil 531} 532 533// GetLabelInOrgByName returns a label by name in given organization. 534func GetLabelInOrgByName(orgID int64, labelName string) (*Label, error) { 535 return getLabelInOrgByName(db.GetEngine(db.DefaultContext), orgID, labelName) 536} 537 538// GetLabelIDsInOrgByNames returns a list of labelIDs by names in a given 539// organization. 540func GetLabelIDsInOrgByNames(orgID int64, labelNames []string) ([]int64, error) { 541 if orgID <= 0 { 542 return nil, ErrOrgLabelNotExist{0, orgID} 543 } 544 labelIDs := make([]int64, 0, len(labelNames)) 545 546 return labelIDs, db.GetEngine(db.DefaultContext).Table("label"). 547 Where("org_id = ?", orgID). 548 In("name", labelNames). 549 Asc("name"). 550 Cols("id"). 551 Find(&labelIDs) 552} 553 554// GetLabelInOrgByID returns a label by ID in given organization. 555func GetLabelInOrgByID(orgID, labelID int64) (*Label, error) { 556 return getLabelInOrgByID(db.GetEngine(db.DefaultContext), orgID, labelID) 557} 558 559// GetLabelsInOrgByIDs returns a list of labels by IDs in given organization, 560// it silently ignores label IDs that do not belong to the organization. 561func GetLabelsInOrgByIDs(orgID int64, labelIDs []int64) ([]*Label, error) { 562 labels := make([]*Label, 0, len(labelIDs)) 563 return labels, db.GetEngine(db.DefaultContext). 564 Where("org_id = ?", orgID). 565 In("id", labelIDs). 566 Asc("name"). 567 Find(&labels) 568} 569 570func getLabelsByOrgID(e db.Engine, orgID int64, sortType string, listOptions db.ListOptions) ([]*Label, error) { 571 if orgID <= 0 { 572 return nil, ErrOrgLabelNotExist{0, orgID} 573 } 574 labels := make([]*Label, 0, 10) 575 sess := e.Where("org_id = ?", orgID) 576 577 switch sortType { 578 case "reversealphabetically": 579 sess.Desc("name") 580 case "leastissues": 581 sess.Asc("num_issues") 582 case "mostissues": 583 sess.Desc("num_issues") 584 default: 585 sess.Asc("name") 586 } 587 588 if listOptions.Page != 0 { 589 sess = db.SetSessionPagination(sess, &listOptions) 590 } 591 592 return labels, sess.Find(&labels) 593} 594 595// GetLabelsByOrgID returns all labels that belong to given organization by ID. 596func GetLabelsByOrgID(orgID int64, sortType string, listOptions db.ListOptions) ([]*Label, error) { 597 return getLabelsByOrgID(db.GetEngine(db.DefaultContext), orgID, sortType, listOptions) 598} 599 600// CountLabelsByOrgID count all labels that belong to given organization by ID. 601func CountLabelsByOrgID(orgID int64) (int64, error) { 602 return db.GetEngine(db.DefaultContext).Where("org_id = ?", orgID).Count(&Label{}) 603} 604 605// .___ 606// | | ______ ________ __ ____ 607// | |/ ___// ___/ | \_/ __ \ 608// | |\___ \ \___ \| | /\ ___/ 609// |___/____ >____ >____/ \___ | 610// \/ \/ \/ 611 612func getLabelsByIssueID(e db.Engine, issueID int64) ([]*Label, error) { 613 var labels []*Label 614 return labels, e.Where("issue_label.issue_id = ?", issueID). 615 Join("LEFT", "issue_label", "issue_label.label_id = label.id"). 616 Asc("label.name"). 617 Find(&labels) 618} 619 620// GetLabelsByIssueID returns all labels that belong to given issue by ID. 621func GetLabelsByIssueID(issueID int64) ([]*Label, error) { 622 return getLabelsByIssueID(db.GetEngine(db.DefaultContext), issueID) 623} 624 625func updateLabelCols(e db.Engine, l *Label, cols ...string) error { 626 _, err := e.ID(l.ID). 627 SetExpr("num_issues", 628 builder.Select("count(*)").From("issue_label"). 629 Where(builder.Eq{"label_id": l.ID}), 630 ). 631 SetExpr("num_closed_issues", 632 builder.Select("count(*)").From("issue_label"). 633 InnerJoin("issue", "issue_label.issue_id = issue.id"). 634 Where(builder.Eq{ 635 "issue_label.label_id": l.ID, 636 "issue.is_closed": true, 637 }), 638 ). 639 Cols(cols...).Update(l) 640 return err 641} 642 643// .___ .____ ___. .__ 644// | | ______ ________ __ ____ | | _____ \_ |__ ____ | | 645// | |/ ___// ___/ | \_/ __ \| | \__ \ | __ \_/ __ \| | 646// | |\___ \ \___ \| | /\ ___/| |___ / __ \| \_\ \ ___/| |__ 647// |___/____ >____ >____/ \___ >_______ (____ /___ /\___ >____/ 648// \/ \/ \/ \/ \/ \/ \/ 649 650// IssueLabel represents an issue-label relation. 651type IssueLabel struct { 652 ID int64 `xorm:"pk autoincr"` 653 IssueID int64 `xorm:"UNIQUE(s)"` 654 LabelID int64 `xorm:"UNIQUE(s)"` 655} 656 657func hasIssueLabel(e db.Engine, issueID, labelID int64) bool { 658 has, _ := e.Where("issue_id = ? AND label_id = ?", issueID, labelID).Get(new(IssueLabel)) 659 return has 660} 661 662// HasIssueLabel returns true if issue has been labeled. 663func HasIssueLabel(issueID, labelID int64) bool { 664 return hasIssueLabel(db.GetEngine(db.DefaultContext), issueID, labelID) 665} 666 667// newIssueLabel this function creates a new label it does not check if the label is valid for the issue 668// YOU MUST CHECK THIS BEFORE THIS FUNCTION 669func newIssueLabel(ctx context.Context, issue *Issue, label *Label, doer *user_model.User) (err error) { 670 e := db.GetEngine(ctx) 671 if _, err = e.Insert(&IssueLabel{ 672 IssueID: issue.ID, 673 LabelID: label.ID, 674 }); err != nil { 675 return err 676 } 677 678 if err = issue.loadRepo(ctx); err != nil { 679 return 680 } 681 682 opts := &CreateCommentOptions{ 683 Type: CommentTypeLabel, 684 Doer: doer, 685 Repo: issue.Repo, 686 Issue: issue, 687 Label: label, 688 Content: "1", 689 } 690 if _, err = createComment(ctx, opts); err != nil { 691 return err 692 } 693 694 return updateLabelCols(e, label, "num_issues", "num_closed_issue") 695} 696 697// NewIssueLabel creates a new issue-label relation. 698func NewIssueLabel(issue *Issue, label *Label, doer *user_model.User) (err error) { 699 if HasIssueLabel(issue.ID, label.ID) { 700 return nil 701 } 702 703 ctx, committer, err := db.TxContext() 704 if err != nil { 705 return err 706 } 707 defer committer.Close() 708 sess := db.GetEngine(ctx) 709 710 if err = issue.loadRepo(ctx); err != nil { 711 return err 712 } 713 714 // Do NOT add invalid labels 715 if issue.RepoID != label.RepoID && issue.Repo.OwnerID != label.OrgID { 716 return nil 717 } 718 719 if err = newIssueLabel(ctx, issue, label, doer); err != nil { 720 return err 721 } 722 723 issue.Labels = nil 724 if err = issue.loadLabels(sess); err != nil { 725 return err 726 } 727 728 return committer.Commit() 729} 730 731// newIssueLabels add labels to an issue. It will check if the labels are valid for the issue 732func newIssueLabels(ctx context.Context, issue *Issue, labels []*Label, doer *user_model.User) (err error) { 733 e := db.GetEngine(ctx) 734 if err = issue.loadRepo(ctx); err != nil { 735 return err 736 } 737 for _, label := range labels { 738 // Don't add already present labels and invalid labels 739 if hasIssueLabel(e, issue.ID, label.ID) || 740 (label.RepoID != issue.RepoID && label.OrgID != issue.Repo.OwnerID) { 741 continue 742 } 743 744 if err = newIssueLabel(ctx, issue, label, doer); err != nil { 745 return fmt.Errorf("newIssueLabel: %v", err) 746 } 747 } 748 749 return nil 750} 751 752// NewIssueLabels creates a list of issue-label relations. 753func NewIssueLabels(issue *Issue, labels []*Label, doer *user_model.User) (err error) { 754 ctx, committer, err := db.TxContext() 755 if err != nil { 756 return err 757 } 758 defer committer.Close() 759 760 if err = newIssueLabels(ctx, issue, labels, doer); err != nil { 761 return err 762 } 763 764 issue.Labels = nil 765 if err = issue.loadLabels(db.GetEngine(ctx)); err != nil { 766 return err 767 } 768 769 return committer.Commit() 770} 771 772func deleteIssueLabel(ctx context.Context, issue *Issue, label *Label, doer *user_model.User) (err error) { 773 e := db.GetEngine(ctx) 774 if count, err := e.Delete(&IssueLabel{ 775 IssueID: issue.ID, 776 LabelID: label.ID, 777 }); err != nil { 778 return err 779 } else if count == 0 { 780 return nil 781 } 782 783 if err = issue.loadRepo(ctx); err != nil { 784 return 785 } 786 787 opts := &CreateCommentOptions{ 788 Type: CommentTypeLabel, 789 Doer: doer, 790 Repo: issue.Repo, 791 Issue: issue, 792 Label: label, 793 } 794 if _, err = createComment(ctx, opts); err != nil { 795 return err 796 } 797 798 return updateLabelCols(e, label, "num_issues", "num_closed_issue") 799} 800 801// DeleteIssueLabel deletes issue-label relation. 802func DeleteIssueLabel(issue *Issue, label *Label, doer *user_model.User) (err error) { 803 ctx, committer, err := db.TxContext() 804 if err != nil { 805 return err 806 } 807 defer committer.Close() 808 809 if err = deleteIssueLabel(ctx, issue, label, doer); err != nil { 810 return err 811 } 812 813 issue.Labels = nil 814 if err = issue.loadLabels(db.GetEngine(ctx)); err != nil { 815 return err 816 } 817 818 return committer.Commit() 819} 820 821func deleteLabelsByRepoID(sess db.Engine, repoID int64) error { 822 deleteCond := builder.Select("id").From("label").Where(builder.Eq{"label.repo_id": repoID}) 823 824 if _, err := sess.In("label_id", deleteCond). 825 Delete(&IssueLabel{}); err != nil { 826 return err 827 } 828 829 _, err := sess.Delete(&Label{RepoID: repoID}) 830 return err 831} 832