2016-02-27 11:31:24 -05:00
// Copyright 2016 The Gogs Authors. All rights reserved.
2020-01-24 19:00:29 +00:00
// Copyright 2020 The Gitea Authors.
2016-02-27 11:31:24 -05:00
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
2022-06-13 17:37:59 +08:00
package issues
2016-02-27 11:31:24 -05:00
import (
2021-09-23 16:45:36 +01:00
"context"
2016-02-27 11:31:24 -05:00
"fmt"
"html/template"
2021-08-25 13:55:47 -04:00
"math"
2016-08-29 20:00:06 -07:00
"regexp"
2016-02-27 11:31:24 -05:00
"strconv"
"strings"
2021-09-19 19:49:59 +08:00
"code.gitea.io/gitea/models/db"
2021-11-24 17:49:20 +08:00
user_model "code.gitea.io/gitea/models/user"
2020-10-13 02:01:57 +02:00
"code.gitea.io/gitea/modules/timeutil"
2019-09-24 21:22:39 +08:00
"xorm.io/builder"
2016-02-27 11:31:24 -05:00
)
2022-06-13 17:37:59 +08:00
// ErrRepoLabelNotExist represents a "RepoLabelNotExist" kind of error.
type ErrRepoLabelNotExist struct {
LabelID int64
RepoID int64
}
// IsErrRepoLabelNotExist checks if an error is a RepoErrLabelNotExist.
func IsErrRepoLabelNotExist ( err error ) bool {
_ , ok := err . ( ErrRepoLabelNotExist )
return ok
}
func ( err ErrRepoLabelNotExist ) Error ( ) string {
return fmt . Sprintf ( "label does not exist [label_id: %d, repo_id: %d]" , err . LabelID , err . RepoID )
}
// ErrOrgLabelNotExist represents a "OrgLabelNotExist" kind of error.
type ErrOrgLabelNotExist struct {
LabelID int64
OrgID int64
}
// IsErrOrgLabelNotExist checks if an error is a OrgErrLabelNotExist.
func IsErrOrgLabelNotExist ( err error ) bool {
_ , ok := err . ( ErrOrgLabelNotExist )
return ok
}
func ( err ErrOrgLabelNotExist ) Error ( ) string {
return fmt . Sprintf ( "label does not exist [label_id: %d, org_id: %d]" , err . LabelID , err . OrgID )
}
// ErrLabelNotExist represents a "LabelNotExist" kind of error.
type ErrLabelNotExist struct {
LabelID int64
}
// IsErrLabelNotExist checks if an error is a ErrLabelNotExist.
func IsErrLabelNotExist ( err error ) bool {
_ , ok := err . ( ErrLabelNotExist )
return ok
}
func ( err ErrLabelNotExist ) Error ( ) string {
return fmt . Sprintf ( "label does not exist [label_id: %d]" , err . LabelID )
}
2020-02-09 15:33:03 +01:00
// LabelColorPattern is a regexp witch can validate LabelColor
2022-02-07 16:21:02 -05:00
var LabelColorPattern = regexp . MustCompile ( "^#?(?:[0-9a-fA-F]{6}|[0-9a-fA-F]{3})$" )
2020-02-09 15:33:03 +01:00
// Label represents a label of repository for issues.
type Label struct {
2020-10-13 02:01:57 +02:00
ID int64 ` xorm:"pk autoincr" `
RepoID int64 ` xorm:"INDEX" `
OrgID int64 ` xorm:"INDEX" `
Name string
Description string
Color string ` xorm:"VARCHAR(7)" `
NumIssues int
NumClosedIssues int
CreatedUnix timeutil . TimeStamp ` xorm:"INDEX created" `
UpdatedUnix timeutil . TimeStamp ` xorm:"INDEX updated" `
2020-04-01 00:14:46 -04:00
NumOpenIssues int ` xorm:"-" `
NumOpenRepoIssues int64 ` xorm:"-" `
IsChecked bool ` xorm:"-" `
QueryString string ` xorm:"-" `
IsSelected bool ` xorm:"-" `
IsExcluded bool ` xorm:"-" `
2020-02-09 15:33:03 +01:00
}
2021-09-19 19:49:59 +08:00
func init ( ) {
db . RegisterModel ( new ( Label ) )
db . RegisterModel ( new ( IssueLabel ) )
}
2020-04-01 00:14:46 -04:00
// CalOpenIssues sets the number of open issues of a label based on the already stored number of closed issues.
2016-08-14 03:32:24 -07:00
func ( label * Label ) CalOpenIssues ( ) {
label . NumOpenIssues = label . NumIssues - label . NumClosedIssues
2016-02-27 11:31:24 -05:00
}
2020-04-01 00:14:46 -04:00
// CalOpenOrgIssues calculates the open issues of a label for a specific repo
func ( label * Label ) CalOpenOrgIssues ( repoID , labelID int64 ) {
counts , _ := CountIssuesByRepo ( & IssuesOptions {
2022-04-25 16:06:24 +02:00
RepoID : repoID ,
LabelIDs : [ ] int64 { labelID } ,
2020-04-01 00:14:46 -04:00
} )
for _ , count := range counts {
label . NumOpenRepoIssues += count
}
}
2019-01-23 06:10:38 +02:00
// LoadSelectedLabelsAfterClick calculates the set of selected labels when a label is clicked
func ( label * Label ) LoadSelectedLabelsAfterClick ( currentSelectedLabels [ ] int64 ) {
var labelQuerySlice [ ] string
labelSelected := false
labelID := strconv . FormatInt ( label . ID , 10 )
for _ , s := range currentSelectedLabels {
if s == label . ID {
labelSelected = true
2019-10-23 19:29:14 +03:00
} else if - s == label . ID {
labelSelected = true
label . IsExcluded = true
} else if s != 0 {
2019-01-23 06:10:38 +02:00
labelQuerySlice = append ( labelQuerySlice , strconv . FormatInt ( s , 10 ) )
}
}
if ! labelSelected {
labelQuerySlice = append ( labelQuerySlice , labelID )
}
label . IsSelected = labelSelected
label . QueryString = strings . Join ( labelQuerySlice , "," )
}
2020-04-01 00:14:46 -04:00
// BelongsToOrg returns true if label is an organization label
func ( label * Label ) BelongsToOrg ( ) bool {
return label . OrgID > 0
}
// BelongsToRepo returns true if label is a repository label
func ( label * Label ) BelongsToRepo ( ) bool {
return label . RepoID > 0
}
2021-08-25 13:55:47 -04:00
// SrgbToLinear converts a component of an sRGB color to its linear intensity
// See: https://en.wikipedia.org/wiki/SRGB#The_reverse_transformation_(sRGB_to_CIE_XYZ)
func SrgbToLinear ( color uint8 ) float64 {
flt := float64 ( color ) / 255
if flt <= 0.04045 {
return flt / 12.92
}
return math . Pow ( ( flt + 0.055 ) / 1.055 , 2.4 )
}
// Luminance returns the luminance of an sRGB color
func Luminance ( color uint32 ) float64 {
r := SrgbToLinear ( uint8 ( 0xFF & ( color >> 16 ) ) )
g := SrgbToLinear ( uint8 ( 0xFF & ( color >> 8 ) ) )
b := SrgbToLinear ( uint8 ( 0xFF & color ) )
// luminance ratios for sRGB
return 0.2126 * r + 0.7152 * g + 0.0722 * b
}
// LuminanceThreshold is the luminance at which white and black appear to have the same contrast
// i.e. x such that 1.05 / (x + 0.05) = (x + 0.05) / 0.05
// i.e. math.Sqrt(1.05*0.05) - 0.05
const LuminanceThreshold float64 = 0.179
2016-02-27 11:31:24 -05:00
// ForegroundColor calculates the text color for labels based
// on their background color.
2016-11-25 09:11:12 +01:00
func ( label * Label ) ForegroundColor ( ) template . CSS {
if strings . HasPrefix ( label . Color , "#" ) {
if color , err := strconv . ParseUint ( label . Color [ 1 : ] , 16 , 64 ) ; err == nil {
2021-08-25 13:55:47 -04:00
// NOTE: see web_src/js/components/ContextPopup.vue for similar implementation
luminance := Luminance ( uint32 ( color ) )
2016-02-27 11:31:24 -05:00
2021-08-25 13:55:47 -04:00
// prefer white or black based upon contrast
if luminance < LuminanceThreshold {
2016-02-27 11:31:24 -05:00
return template . CSS ( "#fff" )
}
2021-08-25 13:55:47 -04:00
return template . CSS ( "#000" )
2016-02-27 11:31:24 -05:00
}
}
// default to black
return template . CSS ( "#000" )
}
2020-04-01 00:14:46 -04:00
// NewLabel creates a new label
2022-03-29 15:23:45 +08:00
func NewLabel ( ctx context . Context , label * Label ) error {
2020-02-09 15:33:03 +01:00
if ! LabelColorPattern . MatchString ( label . Color ) {
return fmt . Errorf ( "bad color code: %s" , label . Color )
}
2022-02-07 16:21:02 -05:00
// normalize case
label . Color = strings . ToLower ( label . Color )
// add leading hash
if label . Color [ 0 ] != '#' {
label . Color = "#" + label . Color
}
// convert 3-character shorthand into 6-character version
if len ( label . Color ) == 4 {
r := label . Color [ 1 ]
g := label . Color [ 2 ]
b := label . Color [ 3 ]
label . Color = fmt . Sprintf ( "#%c%c%c%c%c%c" , r , r , g , g , b , b )
}
2022-03-29 15:23:45 +08:00
return db . Insert ( ctx , label )
2017-06-25 02:15:09 -04:00
}
2020-04-01 00:14:46 -04:00
// NewLabels creates new labels
2017-06-25 02:15:09 -04:00
func NewLabels ( labels ... * Label ) error {
2021-09-19 19:49:59 +08:00
ctx , committer , err := db . TxContext ( )
if err != nil {
2017-06-25 02:15:09 -04:00
return err
}
2021-09-19 19:49:59 +08:00
defer committer . Close ( )
2017-06-25 02:15:09 -04:00
for _ , label := range labels {
2020-02-09 15:33:03 +01:00
if ! LabelColorPattern . MatchString ( label . Color ) {
return fmt . Errorf ( "bad color code: %s" , label . Color )
}
2022-03-29 15:23:45 +08:00
if err := db . Insert ( ctx , label ) ; err != nil {
2017-06-25 02:15:09 -04:00
return err
}
}
2021-09-19 19:49:59 +08:00
return committer . Commit ( )
2017-06-25 02:15:09 -04:00
}
2020-04-01 00:14:46 -04:00
// UpdateLabel updates label information.
func UpdateLabel ( l * Label ) error {
if ! LabelColorPattern . MatchString ( l . Color ) {
return fmt . Errorf ( "bad color code: %s" , l . Color )
}
2022-05-20 22:08:52 +08:00
return updateLabelCols ( db . DefaultContext , l , "name" , "description" , "color" )
2020-04-01 00:14:46 -04:00
}
// DeleteLabel delete a label
func DeleteLabel ( id , labelID int64 ) error {
2022-05-20 22:08:52 +08:00
label , err := GetLabelByID ( db . DefaultContext , labelID )
2020-04-01 00:14:46 -04:00
if err != nil {
if IsErrLabelNotExist ( err ) {
return nil
}
return err
}
2021-11-21 23:41:00 +08:00
ctx , committer , err := db . TxContext ( )
if err != nil {
2020-04-01 00:14:46 -04:00
return err
}
2021-11-21 23:41:00 +08:00
defer committer . Close ( )
sess := db . GetEngine ( ctx )
2020-04-01 00:14:46 -04:00
if label . BelongsToOrg ( ) && label . OrgID != id {
return nil
}
if label . BelongsToRepo ( ) && label . RepoID != id {
return nil
}
if _ , err = sess . ID ( labelID ) . Delete ( new ( Label ) ) ; err != nil {
return err
} else if _ , err = sess .
Where ( "label_id = ?" , labelID ) .
Delete ( new ( IssueLabel ) ) ; err != nil {
return err
}
// delete comments about now deleted label_id
if _ , err = sess . Where ( "label_id = ?" , labelID ) . Cols ( "label_id" ) . Delete ( & Comment { } ) ; err != nil {
return err
}
2021-11-21 23:41:00 +08:00
return committer . Commit ( )
2020-04-01 00:14:46 -04:00
}
2022-05-20 22:08:52 +08:00
// GetLabelByID returns a label by given ID.
func GetLabelByID ( ctx context . Context , labelID int64 ) ( * Label , error ) {
2020-04-01 00:14:46 -04:00
if labelID <= 0 {
return nil , ErrLabelNotExist { labelID }
}
2020-06-18 01:50:11 +08:00
l := & Label { }
2022-05-20 22:08:52 +08:00
has , err := db . GetEngine ( ctx ) . ID ( labelID ) . Get ( l )
2020-04-01 00:14:46 -04:00
if err != nil {
return nil , err
} else if ! has {
return nil , ErrLabelNotExist { l . ID }
}
return l , nil
}
// GetLabelsByIDs returns a list of labels by IDs
func GetLabelsByIDs ( labelIDs [ ] int64 ) ( [ ] * Label , error ) {
labels := make ( [ ] * Label , 0 , len ( labelIDs ) )
2021-09-23 16:45:36 +01:00
return labels , db . GetEngine ( db . DefaultContext ) . Table ( "label" ) .
2020-04-01 00:14:46 -04:00
In ( "id" , labelIDs ) .
Asc ( "name" ) .
2021-03-12 17:45:49 +00:00
Cols ( "id" , "repo_id" , "org_id" ) .
2020-04-01 00:14:46 -04:00
Find ( & labels )
}
// __________ .__ __
// \______ \ ____ ______ ____ _____|__|/ |_ ___________ ___.__.
// | _// __ \\____ \ / _ \/ ___/ \ __\/ _ \_ __ < | |
// | | \ ___/| |_> > <_> )___ \| || | ( <_> ) | \/\___ |
// |____|_ /\___ > __/ \____/____ >__||__| \____/|__| / ____|
// \/ \/|__| \/ \/
2022-05-20 22:08:52 +08:00
// GetLabelInRepoByName returns a label by name in given repository.
func GetLabelInRepoByName ( ctx context . Context , repoID int64 , labelName string ) ( * Label , error ) {
2020-04-01 00:14:46 -04:00
if len ( labelName ) == 0 || repoID <= 0 {
return nil , ErrRepoLabelNotExist { 0 , repoID }
2016-10-07 19:17:27 +02:00
}
l := & Label {
Name : labelName ,
RepoID : repoID ,
}
2022-05-20 22:08:52 +08:00
has , err := db . GetByBean ( ctx , l )
2016-10-07 19:17:27 +02:00
if err != nil {
return nil , err
} else if ! has {
2020-04-01 00:14:46 -04:00
return nil , ErrRepoLabelNotExist { 0 , l . RepoID }
2016-10-07 19:17:27 +02:00
}
return l , nil
}
2022-05-20 22:08:52 +08:00
// GetLabelInRepoByID returns a label by ID in given repository.
func GetLabelInRepoByID ( ctx context . Context , repoID , labelID int64 ) ( * Label , error ) {
2020-04-01 00:14:46 -04:00
if labelID <= 0 || repoID <= 0 {
return nil , ErrRepoLabelNotExist { labelID , repoID }
2016-02-27 11:31:24 -05:00
}
2016-08-03 11:51:22 -07:00
l := & Label {
ID : labelID ,
RepoID : repoID ,
}
2022-05-20 22:08:52 +08:00
has , err := db . GetByBean ( ctx , l )
2016-02-27 11:31:24 -05:00
if err != nil {
return nil , err
} else if ! has {
2020-04-01 00:14:46 -04:00
return nil , ErrRepoLabelNotExist { l . ID , l . RepoID }
2016-02-27 11:31:24 -05:00
}
return l , nil
}
2019-02-04 16:20:44 +01:00
// GetLabelIDsInRepoByNames returns a list of labelIDs by names in a given
// repository.
// it silently ignores label names that do not belong to the repository.
func GetLabelIDsInRepoByNames ( repoID int64 , labelNames [ ] string ) ( [ ] int64 , error ) {
labelIDs := make ( [ ] int64 , 0 , len ( labelNames ) )
2021-09-23 16:45:36 +01:00
return labelIDs , db . GetEngine ( db . DefaultContext ) . Table ( "label" ) .
2019-02-04 16:20:44 +01:00
Where ( "repo_id = ?" , repoID ) .
In ( "name" , labelNames ) .
Asc ( "name" ) .
Cols ( "id" ) .
Find ( & labelIDs )
}
2020-03-30 13:30:39 +08:00
// BuildLabelNamesIssueIDsCondition returns a builder where get issue ids match label names
func BuildLabelNamesIssueIDsCondition ( labelNames [ ] string ) * builder . Builder {
return builder . Select ( "issue_label.issue_id" ) .
From ( "issue_label" ) .
InnerJoin ( "label" , "label.id = issue_label.label_id" ) .
Where (
builder . In ( "label.name" , labelNames ) ,
) .
GroupBy ( "issue_label.issue_id" )
}
2016-08-03 11:51:22 -07:00
// GetLabelsInRepoByIDs returns a list of labels by IDs in given repository,
2019-02-04 16:20:44 +01:00
// it silently ignores label IDs that do not belong to the repository.
2016-08-03 11:51:22 -07:00
func GetLabelsInRepoByIDs ( repoID int64 , labelIDs [ ] int64 ) ( [ ] * Label , error ) {
labels := make ( [ ] * Label , 0 , len ( labelIDs ) )
2021-09-23 16:45:36 +01:00
return labels , db . GetEngine ( db . DefaultContext ) .
2016-11-10 16:16:32 +01:00
Where ( "repo_id = ?" , repoID ) .
2016-11-12 16:29:18 +08:00
In ( "id" , labelIDs ) .
2016-11-10 16:16:32 +01:00
Asc ( "name" ) .
Find ( & labels )
2016-02-27 11:31:24 -05:00
}
2022-05-20 22:08:52 +08:00
// GetLabelsByRepoID returns all labels that belong to given repository by ID.
func GetLabelsByRepoID ( ctx context . Context , repoID int64 , sortType string , listOptions db . ListOptions ) ( [ ] * Label , error ) {
2020-04-01 00:14:46 -04:00
if repoID <= 0 {
return nil , ErrRepoLabelNotExist { 0 , repoID }
}
2016-02-27 11:31:24 -05:00
labels := make ( [ ] * Label , 0 , 10 )
2022-05-20 22:08:52 +08:00
sess := db . GetEngine ( ctx ) . Where ( "repo_id = ?" , repoID )
2016-12-24 15:41:09 +01:00
switch sortType {
case "reversealphabetically" :
sess . Desc ( "name" )
case "leastissues" :
sess . Asc ( "num_issues" )
case "mostissues" :
sess . Desc ( "num_issues" )
default :
sess . Asc ( "name" )
}
2020-01-24 19:00:29 +00:00
if listOptions . Page != 0 {
2021-09-24 19:32:56 +08:00
sess = db . SetSessionPagination ( sess , & listOptions )
2020-01-24 19:00:29 +00:00
}
2016-12-24 15:41:09 +01:00
return labels , sess . Find ( & labels )
2016-02-27 11:31:24 -05:00
}
2021-08-12 14:43:08 +02:00
// CountLabelsByRepoID count number of all labels that belong to given repository by ID.
func CountLabelsByRepoID ( repoID int64 ) ( int64 , error ) {
2021-09-23 16:45:36 +01:00
return db . GetEngine ( db . DefaultContext ) . Where ( "repo_id = ?" , repoID ) . Count ( & Label { } )
2021-08-12 14:43:08 +02:00
}
2020-04-01 00:14:46 -04:00
// ________
// \_____ \_______ ____
// / | \_ __ \/ ___\
// / | \ | \/ /_/ >
// \_______ /__| \___ /
// \/ /_____/
2022-05-20 22:08:52 +08:00
// GetLabelInOrgByName returns a label by name in given organization.
func GetLabelInOrgByName ( ctx context . Context , orgID int64 , labelName string ) ( * Label , error ) {
2020-04-01 00:14:46 -04:00
if len ( labelName ) == 0 || orgID <= 0 {
return nil , ErrOrgLabelNotExist { 0 , orgID }
}
l := & Label {
Name : labelName ,
OrgID : orgID ,
}
2022-05-20 22:08:52 +08:00
has , err := db . GetByBean ( ctx , l )
2020-04-01 00:14:46 -04:00
if err != nil {
return nil , err
} else if ! has {
return nil , ErrOrgLabelNotExist { 0 , l . OrgID }
}
return l , nil
}
2022-05-20 22:08:52 +08:00
// GetLabelInOrgByID returns a label by ID in given organization.
func GetLabelInOrgByID ( ctx context . Context , orgID , labelID int64 ) ( * Label , error ) {
2020-04-01 00:14:46 -04:00
if labelID <= 0 || orgID <= 0 {
return nil , ErrOrgLabelNotExist { labelID , orgID }
}
l := & Label {
ID : labelID ,
OrgID : orgID ,
}
2022-05-20 22:08:52 +08:00
has , err := db . GetByBean ( ctx , l )
2020-04-01 00:14:46 -04:00
if err != nil {
return nil , err
} else if ! has {
return nil , ErrOrgLabelNotExist { l . ID , l . OrgID }
}
return l , nil
}
// GetLabelIDsInOrgByNames returns a list of labelIDs by names in a given
// organization.
func GetLabelIDsInOrgByNames ( orgID int64 , labelNames [ ] string ) ( [ ] int64 , error ) {
if orgID <= 0 {
return nil , ErrOrgLabelNotExist { 0 , orgID }
}
labelIDs := make ( [ ] int64 , 0 , len ( labelNames ) )
2021-09-23 16:45:36 +01:00
return labelIDs , db . GetEngine ( db . DefaultContext ) . Table ( "label" ) .
2020-04-01 00:14:46 -04:00
Where ( "org_id = ?" , orgID ) .
In ( "name" , labelNames ) .
Asc ( "name" ) .
Cols ( "id" ) .
Find ( & labelIDs )
}
// GetLabelsInOrgByIDs returns a list of labels by IDs in given organization,
// it silently ignores label IDs that do not belong to the organization.
func GetLabelsInOrgByIDs ( orgID int64 , labelIDs [ ] int64 ) ( [ ] * Label , error ) {
labels := make ( [ ] * Label , 0 , len ( labelIDs ) )
2021-09-23 16:45:36 +01:00
return labels , db . GetEngine ( db . DefaultContext ) .
2020-04-01 00:14:46 -04:00
Where ( "org_id = ?" , orgID ) .
In ( "id" , labelIDs ) .
Asc ( "name" ) .
Find ( & labels )
}
2022-05-20 22:08:52 +08:00
// GetLabelsByOrgID returns all labels that belong to given organization by ID.
func GetLabelsByOrgID ( ctx context . Context , orgID int64 , sortType string , listOptions db . ListOptions ) ( [ ] * Label , error ) {
2020-04-01 00:14:46 -04:00
if orgID <= 0 {
return nil , ErrOrgLabelNotExist { 0 , orgID }
}
labels := make ( [ ] * Label , 0 , 10 )
2022-05-20 22:08:52 +08:00
sess := db . GetEngine ( ctx ) . Where ( "org_id = ?" , orgID )
2020-04-01 00:14:46 -04:00
switch sortType {
case "reversealphabetically" :
sess . Desc ( "name" )
case "leastissues" :
sess . Asc ( "num_issues" )
case "mostissues" :
sess . Desc ( "num_issues" )
default :
sess . Asc ( "name" )
}
if listOptions . Page != 0 {
2021-09-24 19:32:56 +08:00
sess = db . SetSessionPagination ( sess , & listOptions )
2020-04-01 00:14:46 -04:00
}
return labels , sess . Find ( & labels )
}
2021-08-12 14:43:08 +02:00
// CountLabelsByOrgID count all labels that belong to given organization by ID.
func CountLabelsByOrgID ( orgID int64 ) ( int64 , error ) {
2021-09-23 16:45:36 +01:00
return db . GetEngine ( db . DefaultContext ) . Where ( "org_id = ?" , orgID ) . Count ( & Label { } )
2021-08-12 14:43:08 +02:00
}
2020-04-01 00:14:46 -04:00
// .___
// | | ______ ________ __ ____
// | |/ ___// ___/ | \_/ __ \
// | |\___ \ \___ \| | /\ ___/
// |___/____ >____ >____/ \___ |
// \/ \/ \/
2022-05-20 22:08:52 +08:00
// GetLabelsByIssueID returns all labels that belong to given issue by ID.
func GetLabelsByIssueID ( ctx context . Context , issueID int64 ) ( [ ] * Label , error ) {
2017-02-11 20:01:33 +08:00
var labels [ ] * Label
2022-05-20 22:08:52 +08:00
return labels , db . GetEngine ( ctx ) . Where ( "issue_label.issue_id = ?" , issueID ) .
2017-02-11 20:01:33 +08:00
Join ( "LEFT" , "issue_label" , "issue_label.label_id = label.id" ) .
Asc ( "label.name" ) .
2016-11-10 16:16:32 +01:00
Find ( & labels )
2016-02-27 11:31:24 -05:00
}
2022-05-20 22:08:52 +08:00
func updateLabelCols ( ctx context . Context , l * Label , cols ... string ) error {
_ , err := db . GetEngine ( ctx ) . ID ( l . ID ) .
2019-09-24 21:22:39 +08:00
SetExpr ( "num_issues" ,
builder . Select ( "count(*)" ) . From ( "issue_label" ) .
Where ( builder . Eq { "label_id" : l . ID } ) ,
) .
SetExpr ( "num_closed_issues" ,
builder . Select ( "count(*)" ) . From ( "issue_label" ) .
InnerJoin ( "issue" , "issue_label.issue_id = issue.id" ) .
Where ( builder . Eq {
"issue_label.label_id" : l . ID ,
"issue.is_closed" : true ,
} ) ,
) .
2020-04-29 01:28:56 +02:00
Cols ( cols ... ) . Update ( l )
2016-02-27 11:31:24 -05:00
return err
}
// .___ .____ ___. .__
// | | ______ ________ __ ____ | | _____ \_ |__ ____ | |
// | |/ ___// ___/ | \_/ __ \| | \__ \ | __ \_/ __ \| |
// | |\___ \ \___ \| | /\ ___/| |___ / __ \| \_\ \ ___/| |__
// |___/____ >____ >____/ \___ >_______ (____ /___ /\___ >____/
// \/ \/ \/ \/ \/ \/ \/
2017-01-04 19:50:34 -05:00
// IssueLabel represents an issue-label relation.
2016-02-27 11:31:24 -05:00
type IssueLabel struct {
ID int64 ` xorm:"pk autoincr" `
IssueID int64 ` xorm:"UNIQUE(s)" `
LabelID int64 ` xorm:"UNIQUE(s)" `
}
// HasIssueLabel returns true if issue has been labeled.
2022-05-20 22:08:52 +08:00
func HasIssueLabel ( ctx context . Context , issueID , labelID int64 ) bool {
has , _ := db . GetEngine ( ctx ) . Where ( "issue_id = ? AND label_id = ?" , issueID , labelID ) . Get ( new ( IssueLabel ) )
return has
2016-02-27 11:31:24 -05:00
}
2021-03-12 17:45:49 +00:00
// newIssueLabel this function creates a new label it does not check if the label is valid for the issue
// YOU MUST CHECK THIS BEFORE THIS FUNCTION
2021-11-24 17:49:20 +08:00
func newIssueLabel ( ctx context . Context , issue * Issue , label * Label , doer * user_model . User ) ( err error ) {
2022-05-20 22:08:52 +08:00
if err = db . Insert ( ctx , & IssueLabel {
2016-02-27 11:31:24 -05:00
IssueID : issue . ID ,
LabelID : label . ID ,
} ) ; err != nil {
return err
}
2022-04-08 17:11:15 +08:00
if err = issue . LoadRepo ( ctx ) ; err != nil {
2017-01-30 20:46:45 +08:00
return
}
2021-03-15 02:52:12 +08:00
opts := & CreateCommentOptions {
2019-11-16 02:18:09 +08:00
Type : CommentTypeLabel ,
Doer : doer ,
Repo : issue . Repo ,
Issue : issue ,
Label : label ,
Content : "1" ,
2019-12-01 10:44:39 +08:00
}
2022-04-08 17:11:15 +08:00
if _ , err = CreateCommentCtx ( ctx , opts ) ; err != nil {
2017-01-30 20:46:45 +08:00
return err
}
2022-05-20 22:08:52 +08:00
return updateLabelCols ( ctx , label , "num_issues" , "num_closed_issue" )
2016-02-27 11:31:24 -05:00
}
// NewIssueLabel creates a new issue-label relation.
2021-11-24 17:49:20 +08:00
func NewIssueLabel ( issue * Issue , label * Label , doer * user_model . User ) ( err error ) {
2022-05-20 22:08:52 +08:00
if HasIssueLabel ( db . DefaultContext , issue . ID , label . ID ) {
2016-08-03 11:51:22 -07:00
return nil
}
2021-11-19 21:39:57 +08:00
ctx , committer , err := db . TxContext ( )
if err != nil {
2016-02-27 11:31:24 -05:00
return err
}
2021-11-19 21:39:57 +08:00
defer committer . Close ( )
2016-02-27 11:31:24 -05:00
2022-04-08 17:11:15 +08:00
if err = issue . LoadRepo ( ctx ) ; err != nil {
2021-03-12 17:45:49 +00:00
return err
}
// Do NOT add invalid labels
if issue . RepoID != label . RepoID && issue . Repo . OwnerID != label . OrgID {
return nil
}
2021-11-19 21:39:57 +08:00
if err = newIssueLabel ( ctx , issue , label , doer ) ; err != nil {
2016-02-27 11:31:24 -05:00
return err
2016-08-03 11:51:22 -07:00
}
2020-10-05 07:49:11 +01:00
issue . Labels = nil
2022-04-28 13:48:48 +02:00
if err = issue . LoadLabels ( ctx ) ; err != nil {
2020-10-05 07:49:11 +01:00
return err
}
2021-11-19 21:39:57 +08:00
return committer . Commit ( )
2016-08-03 11:51:22 -07:00
}
2021-03-12 17:45:49 +00:00
// newIssueLabels add labels to an issue. It will check if the labels are valid for the issue
2021-11-24 17:49:20 +08:00
func newIssueLabels ( ctx context . Context , issue * Issue , labels [ ] * Label , doer * user_model . User ) ( err error ) {
2022-04-08 17:11:15 +08:00
if err = issue . LoadRepo ( ctx ) ; err != nil {
2021-03-12 17:45:49 +00:00
return err
}
for _ , label := range labels {
// Don't add already present labels and invalid labels
2022-05-20 22:08:52 +08:00
if HasIssueLabel ( ctx , issue . ID , label . ID ) ||
2021-03-12 17:45:49 +00:00
( label . RepoID != issue . RepoID && label . OrgID != issue . Repo . OwnerID ) {
2016-08-03 11:51:22 -07:00
continue
}
2021-11-19 21:39:57 +08:00
if err = newIssueLabel ( ctx , issue , label , doer ) ; err != nil {
2016-08-03 11:51:22 -07:00
return fmt . Errorf ( "newIssueLabel: %v" , err )
}
}
return nil
}
// NewIssueLabels creates a list of issue-label relations.
2021-11-24 17:49:20 +08:00
func NewIssueLabels ( issue * Issue , labels [ ] * Label , doer * user_model . User ) ( err error ) {
2021-09-19 19:49:59 +08:00
ctx , committer , err := db . TxContext ( )
if err != nil {
2016-08-03 11:51:22 -07:00
return err
}
2021-09-19 19:49:59 +08:00
defer committer . Close ( )
2016-08-03 11:51:22 -07:00
2021-11-19 21:39:57 +08:00
if err = newIssueLabels ( ctx , issue , labels , doer ) ; err != nil {
2016-08-03 11:51:22 -07:00
return err
2016-02-27 11:31:24 -05:00
}
2020-10-05 07:49:11 +01:00
issue . Labels = nil
2022-04-28 13:48:48 +02:00
if err = issue . LoadLabels ( ctx ) ; err != nil {
2020-10-05 07:49:11 +01:00
return err
}
2021-09-19 19:49:59 +08:00
return committer . Commit ( )
2016-02-27 11:31:24 -05:00
}
2021-11-24 17:49:20 +08:00
func deleteIssueLabel ( ctx context . Context , issue * Issue , label * Label , doer * user_model . User ) ( err error ) {
2022-05-20 22:08:52 +08:00
if count , err := db . DeleteByBean ( ctx , & IssueLabel {
2016-02-27 11:31:24 -05:00
IssueID : issue . ID ,
LabelID : label . ID ,
} ) ; err != nil {
return err
2017-01-31 20:31:35 -05:00
} else if count == 0 {
return nil
2016-02-27 11:31:24 -05:00
}
2022-04-08 17:11:15 +08:00
if err = issue . LoadRepo ( ctx ) ; err != nil {
2017-01-30 20:46:45 +08:00
return
}
2021-03-15 02:52:12 +08:00
opts := & CreateCommentOptions {
2019-11-16 02:18:09 +08:00
Type : CommentTypeLabel ,
Doer : doer ,
Repo : issue . Repo ,
Issue : issue ,
Label : label ,
2019-12-01 10:44:39 +08:00
}
2022-04-08 17:11:15 +08:00
if _ , err = CreateCommentCtx ( ctx , opts ) ; err != nil {
2017-01-30 20:46:45 +08:00
return err
}
2022-05-20 22:08:52 +08:00
return updateLabelCols ( ctx , label , "num_issues" , "num_closed_issue" )
2016-02-27 11:31:24 -05:00
}
// DeleteIssueLabel deletes issue-label relation.
2022-04-28 13:48:48 +02:00
func DeleteIssueLabel ( ctx context . Context , issue * Issue , label * Label , doer * user_model . User ) error {
if err := deleteIssueLabel ( ctx , issue , label , doer ) ; err != nil {
2016-02-27 11:31:24 -05:00
return err
}
2020-10-05 07:49:11 +01:00
issue . Labels = nil
2022-04-28 13:48:48 +02:00
return issue . LoadLabels ( ctx )
2016-02-27 11:31:24 -05:00
}
2021-03-19 20:01:24 +01:00
2022-06-13 17:37:59 +08:00
// DeleteLabelsByRepoID deletes labels of some repository
func DeleteLabelsByRepoID ( ctx context . Context , repoID int64 ) error {
2021-03-19 20:01:24 +01:00
deleteCond := builder . Select ( "id" ) . From ( "label" ) . Where ( builder . Eq { "label.repo_id" : repoID } )
2022-05-20 22:08:52 +08:00
if _ , err := db . GetEngine ( ctx ) . In ( "label_id" , deleteCond ) .
2021-03-19 20:01:24 +01:00
Delete ( & IssueLabel { } ) ; err != nil {
return err
}
2022-05-20 22:08:52 +08:00
_ , err := db . DeleteByBean ( ctx , & Label { RepoID : repoID } )
2021-03-19 20:01:24 +01:00
return err
}
2022-06-13 17:37:59 +08:00
// CountOrphanedLabels return count of labels witch are broken and not accessible via ui anymore
func CountOrphanedLabels ( ) ( int64 , error ) {
2022-06-17 22:42:51 +01:00
noref , err := db . GetEngine ( db . DefaultContext ) . Table ( "label" ) . Where ( "repo_id=? AND org_id=?" , 0 , 0 ) . Count ( )
2022-06-13 17:37:59 +08:00
if err != nil {
return 0 , err
}
norepo , err := db . GetEngine ( db . DefaultContext ) . Table ( "label" ) .
Where ( builder . And (
builder . Gt { "repo_id" : 0 } ,
builder . NotIn ( "repo_id" , builder . Select ( "id" ) . From ( "repository" ) ) ,
) ) .
Count ( )
if err != nil {
return 0 , err
}
noorg , err := db . GetEngine ( db . DefaultContext ) . Table ( "label" ) .
Where ( builder . And (
builder . Gt { "org_id" : 0 } ,
builder . NotIn ( "org_id" , builder . Select ( "id" ) . From ( "user" ) ) ,
) ) .
Count ( )
if err != nil {
return 0 , err
}
return noref + norepo + noorg , nil
}
// DeleteOrphanedLabels delete labels witch are broken and not accessible via ui anymore
func DeleteOrphanedLabels ( ) error {
// delete labels with no reference
if _ , err := db . GetEngine ( db . DefaultContext ) . Table ( "label" ) . Where ( "repo_id=? AND org_id=?" , 0 , 0 ) . Delete ( new ( Label ) ) ; err != nil {
return err
}
// delete labels with none existing repos
if _ , err := db . GetEngine ( db . DefaultContext ) .
Where ( builder . And (
builder . Gt { "repo_id" : 0 } ,
builder . NotIn ( "repo_id" , builder . Select ( "id" ) . From ( "repository" ) ) ,
) ) .
Delete ( Label { } ) ; err != nil {
return err
}
// delete labels with none existing orgs
if _ , err := db . GetEngine ( db . DefaultContext ) .
Where ( builder . And (
builder . Gt { "org_id" : 0 } ,
builder . NotIn ( "org_id" , builder . Select ( "id" ) . From ( "user" ) ) ,
) ) .
Delete ( Label { } ) ; err != nil {
return err
}
return nil
}
// CountOrphanedIssueLabels return count of IssueLabels witch have no label behind anymore
func CountOrphanedIssueLabels ( ) ( int64 , error ) {
return db . GetEngine ( db . DefaultContext ) . Table ( "issue_label" ) .
NotIn ( "label_id" , builder . Select ( "id" ) . From ( "label" ) ) .
Count ( )
}
// DeleteOrphanedIssueLabels delete IssueLabels witch have no label behind anymore
func DeleteOrphanedIssueLabels ( ) error {
_ , err := db . GetEngine ( db . DefaultContext ) .
NotIn ( "label_id" , builder . Select ( "id" ) . From ( "label" ) ) .
Delete ( IssueLabel { } )
return err
}
// CountIssueLabelWithOutsideLabels count label comments with outside label
func CountIssueLabelWithOutsideLabels ( ) ( int64 , error ) {
return db . GetEngine ( db . DefaultContext ) . Where ( builder . Expr ( "(label.org_id = 0 AND issue.repo_id != label.repo_id) OR (label.repo_id = 0 AND label.org_id != repository.owner_id)" ) ) .
Table ( "issue_label" ) .
Join ( "inner" , "label" , "issue_label.label_id = label.id " ) .
Join ( "inner" , "issue" , "issue.id = issue_label.issue_id " ) .
Join ( "inner" , "repository" , "issue.repo_id = repository.id" ) .
Count ( new ( IssueLabel ) )
}
// FixIssueLabelWithOutsideLabels fix label comments with outside label
func FixIssueLabelWithOutsideLabels ( ) ( int64 , error ) {
res , err := db . GetEngine ( db . DefaultContext ) . Exec ( ` DELETE FROM issue_label WHERE issue_label.id IN (
SELECT il_too.id FROM (
SELECT il_too_too.id
FROM issue_label AS il_too_too
INNER JOIN label ON il_too_too.label_id = label.id
INNER JOIN issue on issue.id = il_too_too.issue_id
INNER JOIN repository on repository.id = issue.repo_id
WHERE
(label.org_id = 0 AND issue.repo_id != label.repo_id) OR (label.repo_id = 0 AND label.org_id != repository.owner_id)
) AS il_too ) ` )
if err != nil {
return 0 , err
}
return res . RowsAffected ( )
}