code.gitea.io/gitea@v1.22.3/models/issues/issue_label.go (about) 1 // Copyright 2023 The Gitea Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package issues 5 6 import ( 7 "context" 8 "fmt" 9 "sort" 10 11 "code.gitea.io/gitea/models/db" 12 access_model "code.gitea.io/gitea/models/perm/access" 13 user_model "code.gitea.io/gitea/models/user" 14 15 "xorm.io/builder" 16 ) 17 18 // IssueLabel represents an issue-label relation. 19 type IssueLabel struct { 20 ID int64 `xorm:"pk autoincr"` 21 IssueID int64 `xorm:"UNIQUE(s)"` 22 LabelID int64 `xorm:"UNIQUE(s)"` 23 } 24 25 // HasIssueLabel returns true if issue has been labeled. 26 func HasIssueLabel(ctx context.Context, issueID, labelID int64) bool { 27 has, _ := db.GetEngine(ctx).Where("issue_id = ? AND label_id = ?", issueID, labelID).Get(new(IssueLabel)) 28 return has 29 } 30 31 // newIssueLabel this function creates a new label it does not check if the label is valid for the issue 32 // YOU MUST CHECK THIS BEFORE THIS FUNCTION 33 func newIssueLabel(ctx context.Context, issue *Issue, label *Label, doer *user_model.User) (err error) { 34 if err = db.Insert(ctx, &IssueLabel{ 35 IssueID: issue.ID, 36 LabelID: label.ID, 37 }); err != nil { 38 return err 39 } 40 41 if err = issue.LoadRepo(ctx); err != nil { 42 return err 43 } 44 45 opts := &CreateCommentOptions{ 46 Type: CommentTypeLabel, 47 Doer: doer, 48 Repo: issue.Repo, 49 Issue: issue, 50 Label: label, 51 Content: "1", 52 } 53 if _, err = CreateComment(ctx, opts); err != nil { 54 return err 55 } 56 57 issue.Labels = append(issue.Labels, label) 58 59 return updateLabelCols(ctx, label, "num_issues", "num_closed_issue") 60 } 61 62 // Remove all issue labels in the given exclusive scope 63 func RemoveDuplicateExclusiveIssueLabels(ctx context.Context, issue *Issue, label *Label, doer *user_model.User) (err error) { 64 scope := label.ExclusiveScope() 65 if scope == "" { 66 return nil 67 } 68 69 var toRemove []*Label 70 for _, issueLabel := range issue.Labels { 71 if label.ID != issueLabel.ID && issueLabel.ExclusiveScope() == scope { 72 toRemove = append(toRemove, issueLabel) 73 } 74 } 75 76 for _, issueLabel := range toRemove { 77 if err = deleteIssueLabel(ctx, issue, issueLabel, doer); err != nil { 78 return err 79 } 80 } 81 82 return nil 83 } 84 85 // NewIssueLabel creates a new issue-label relation. 86 func NewIssueLabel(ctx context.Context, issue *Issue, label *Label, doer *user_model.User) (err error) { 87 if HasIssueLabel(ctx, issue.ID, label.ID) { 88 return nil 89 } 90 91 ctx, committer, err := db.TxContext(ctx) 92 if err != nil { 93 return err 94 } 95 defer committer.Close() 96 97 if err = issue.LoadRepo(ctx); err != nil { 98 return err 99 } 100 101 // Do NOT add invalid labels 102 if issue.RepoID != label.RepoID && issue.Repo.OwnerID != label.OrgID { 103 return nil 104 } 105 106 if err = RemoveDuplicateExclusiveIssueLabels(ctx, issue, label, doer); err != nil { 107 return nil 108 } 109 110 if err = newIssueLabel(ctx, issue, label, doer); err != nil { 111 return err 112 } 113 114 issue.Labels = nil 115 if err = issue.LoadLabels(ctx); err != nil { 116 return err 117 } 118 119 return committer.Commit() 120 } 121 122 // newIssueLabels add labels to an issue. It will check if the labels are valid for the issue 123 func newIssueLabels(ctx context.Context, issue *Issue, labels []*Label, doer *user_model.User) (err error) { 124 if err = issue.LoadRepo(ctx); err != nil { 125 return err 126 } 127 128 if err = issue.LoadLabels(ctx); err != nil { 129 return err 130 } 131 132 for _, l := range labels { 133 // Don't add already present labels and invalid labels 134 if HasIssueLabel(ctx, issue.ID, l.ID) || 135 (l.RepoID != issue.RepoID && l.OrgID != issue.Repo.OwnerID) { 136 continue 137 } 138 139 if err = RemoveDuplicateExclusiveIssueLabels(ctx, issue, l, doer); err != nil { 140 return err 141 } 142 143 if err = newIssueLabel(ctx, issue, l, doer); err != nil { 144 return fmt.Errorf("newIssueLabel: %w", err) 145 } 146 } 147 148 return nil 149 } 150 151 // NewIssueLabels creates a list of issue-label relations. 152 func NewIssueLabels(ctx context.Context, issue *Issue, labels []*Label, doer *user_model.User) (err error) { 153 ctx, committer, err := db.TxContext(ctx) 154 if err != nil { 155 return err 156 } 157 defer committer.Close() 158 159 if err = newIssueLabels(ctx, issue, labels, doer); err != nil { 160 return err 161 } 162 163 issue.Labels = nil 164 if err = issue.LoadLabels(ctx); err != nil { 165 return err 166 } 167 168 return committer.Commit() 169 } 170 171 func deleteIssueLabel(ctx context.Context, issue *Issue, label *Label, doer *user_model.User) (err error) { 172 if count, err := db.DeleteByBean(ctx, &IssueLabel{ 173 IssueID: issue.ID, 174 LabelID: label.ID, 175 }); err != nil { 176 return err 177 } else if count == 0 { 178 return nil 179 } 180 181 if err = issue.LoadRepo(ctx); err != nil { 182 return err 183 } 184 185 opts := &CreateCommentOptions{ 186 Type: CommentTypeLabel, 187 Doer: doer, 188 Repo: issue.Repo, 189 Issue: issue, 190 Label: label, 191 } 192 if _, err = CreateComment(ctx, opts); err != nil { 193 return err 194 } 195 196 return updateLabelCols(ctx, label, "num_issues", "num_closed_issue") 197 } 198 199 // DeleteIssueLabel deletes issue-label relation. 200 func DeleteIssueLabel(ctx context.Context, issue *Issue, label *Label, doer *user_model.User) error { 201 if err := deleteIssueLabel(ctx, issue, label, doer); err != nil { 202 return err 203 } 204 205 issue.Labels = nil 206 return issue.LoadLabels(ctx) 207 } 208 209 // DeleteLabelsByRepoID deletes labels of some repository 210 func DeleteLabelsByRepoID(ctx context.Context, repoID int64) error { 211 deleteCond := builder.Select("id").From("label").Where(builder.Eq{"label.repo_id": repoID}) 212 213 if _, err := db.GetEngine(ctx).In("label_id", deleteCond). 214 Delete(&IssueLabel{}); err != nil { 215 return err 216 } 217 218 _, err := db.DeleteByBean(ctx, &Label{RepoID: repoID}) 219 return err 220 } 221 222 // CountOrphanedLabels return count of labels witch are broken and not accessible via ui anymore 223 func CountOrphanedLabels(ctx context.Context) (int64, error) { 224 noref, err := db.GetEngine(ctx).Table("label").Where("repo_id=? AND org_id=?", 0, 0).Count() 225 if err != nil { 226 return 0, err 227 } 228 229 norepo, err := db.GetEngine(ctx).Table("label"). 230 Where(builder.And( 231 builder.Gt{"repo_id": 0}, 232 builder.NotIn("repo_id", builder.Select("id").From("`repository`")), 233 )). 234 Count() 235 if err != nil { 236 return 0, err 237 } 238 239 noorg, err := db.GetEngine(ctx).Table("label"). 240 Where(builder.And( 241 builder.Gt{"org_id": 0}, 242 builder.NotIn("org_id", builder.Select("id").From("`user`")), 243 )). 244 Count() 245 if err != nil { 246 return 0, err 247 } 248 249 return noref + norepo + noorg, nil 250 } 251 252 // DeleteOrphanedLabels delete labels witch are broken and not accessible via ui anymore 253 func DeleteOrphanedLabels(ctx context.Context) error { 254 // delete labels with no reference 255 if _, err := db.GetEngine(ctx).Table("label").Where("repo_id=? AND org_id=?", 0, 0).Delete(new(Label)); err != nil { 256 return err 257 } 258 259 // delete labels with none existing repos 260 if _, err := db.GetEngine(ctx). 261 Where(builder.And( 262 builder.Gt{"repo_id": 0}, 263 builder.NotIn("repo_id", builder.Select("id").From("`repository`")), 264 )). 265 Delete(Label{}); err != nil { 266 return err 267 } 268 269 // delete labels with none existing orgs 270 if _, err := db.GetEngine(ctx). 271 Where(builder.And( 272 builder.Gt{"org_id": 0}, 273 builder.NotIn("org_id", builder.Select("id").From("`user`")), 274 )). 275 Delete(Label{}); err != nil { 276 return err 277 } 278 279 return nil 280 } 281 282 // CountOrphanedIssueLabels return count of IssueLabels witch have no label behind anymore 283 func CountOrphanedIssueLabels(ctx context.Context) (int64, error) { 284 return db.GetEngine(ctx).Table("issue_label"). 285 NotIn("label_id", builder.Select("id").From("label")). 286 Count() 287 } 288 289 // DeleteOrphanedIssueLabels delete IssueLabels witch have no label behind anymore 290 func DeleteOrphanedIssueLabels(ctx context.Context) error { 291 _, err := db.GetEngine(ctx). 292 NotIn("label_id", builder.Select("id").From("label")). 293 Delete(IssueLabel{}) 294 return err 295 } 296 297 // CountIssueLabelWithOutsideLabels count label comments with outside label 298 func CountIssueLabelWithOutsideLabels(ctx context.Context) (int64, error) { 299 return db.GetEngine(ctx).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)")). 300 Table("issue_label"). 301 Join("inner", "label", "issue_label.label_id = label.id "). 302 Join("inner", "issue", "issue.id = issue_label.issue_id "). 303 Join("inner", "repository", "issue.repo_id = repository.id"). 304 Count(new(IssueLabel)) 305 } 306 307 // FixIssueLabelWithOutsideLabels fix label comments with outside label 308 func FixIssueLabelWithOutsideLabels(ctx context.Context) (int64, error) { 309 res, err := db.GetEngine(ctx).Exec(`DELETE FROM issue_label WHERE issue_label.id IN ( 310 SELECT il_too.id FROM ( 311 SELECT il_too_too.id 312 FROM issue_label AS il_too_too 313 INNER JOIN label ON il_too_too.label_id = label.id 314 INNER JOIN issue on issue.id = il_too_too.issue_id 315 INNER JOIN repository on repository.id = issue.repo_id 316 WHERE 317 (label.org_id = 0 AND issue.repo_id != label.repo_id) OR (label.repo_id = 0 AND label.org_id != repository.owner_id) 318 ) AS il_too )`) 319 if err != nil { 320 return 0, err 321 } 322 323 return res.RowsAffected() 324 } 325 326 // LoadLabels loads labels 327 func (issue *Issue) LoadLabels(ctx context.Context) (err error) { 328 if issue.Labels == nil && issue.ID != 0 { 329 issue.Labels, err = GetLabelsByIssueID(ctx, issue.ID) 330 if err != nil { 331 return fmt.Errorf("getLabelsByIssueID [%d]: %w", issue.ID, err) 332 } 333 } 334 return nil 335 } 336 337 // GetLabelsByIssueID returns all labels that belong to given issue by ID. 338 func GetLabelsByIssueID(ctx context.Context, issueID int64) ([]*Label, error) { 339 var labels []*Label 340 return labels, db.GetEngine(ctx).Where("issue_label.issue_id = ?", issueID). 341 Join("LEFT", "issue_label", "issue_label.label_id = label.id"). 342 Asc("label.name"). 343 Find(&labels) 344 } 345 346 func clearIssueLabels(ctx context.Context, issue *Issue, doer *user_model.User) (err error) { 347 if err = issue.LoadLabels(ctx); err != nil { 348 return fmt.Errorf("getLabels: %w", err) 349 } 350 351 for i := range issue.Labels { 352 if err = deleteIssueLabel(ctx, issue, issue.Labels[i], doer); err != nil { 353 return fmt.Errorf("removeLabel: %w", err) 354 } 355 } 356 357 return nil 358 } 359 360 // ClearIssueLabels removes all issue labels as the given user. 361 // Triggers appropriate WebHooks, if any. 362 func ClearIssueLabels(ctx context.Context, issue *Issue, doer *user_model.User) (err error) { 363 ctx, committer, err := db.TxContext(ctx) 364 if err != nil { 365 return err 366 } 367 defer committer.Close() 368 369 if err := issue.LoadRepo(ctx); err != nil { 370 return err 371 } else if err = issue.LoadPullRequest(ctx); err != nil { 372 return err 373 } 374 375 perm, err := access_model.GetUserRepoPermission(ctx, issue.Repo, doer) 376 if err != nil { 377 return err 378 } 379 if !perm.CanWriteIssuesOrPulls(issue.IsPull) { 380 return ErrRepoLabelNotExist{} 381 } 382 383 if err = clearIssueLabels(ctx, issue, doer); err != nil { 384 return err 385 } 386 387 if err = committer.Commit(); err != nil { 388 return fmt.Errorf("Commit: %w", err) 389 } 390 391 return nil 392 } 393 394 type labelSorter []*Label 395 396 func (ts labelSorter) Len() int { 397 return len([]*Label(ts)) 398 } 399 400 func (ts labelSorter) Less(i, j int) bool { 401 return []*Label(ts)[i].ID < []*Label(ts)[j].ID 402 } 403 404 func (ts labelSorter) Swap(i, j int) { 405 []*Label(ts)[i], []*Label(ts)[j] = []*Label(ts)[j], []*Label(ts)[i] 406 } 407 408 // Ensure only one label of a given scope exists, with labels at the end of the 409 // array getting preference over earlier ones. 410 func RemoveDuplicateExclusiveLabels(labels []*Label) []*Label { 411 validLabels := make([]*Label, 0, len(labels)) 412 413 for i, label := range labels { 414 scope := label.ExclusiveScope() 415 if scope != "" { 416 foundOther := false 417 for _, otherLabel := range labels[i+1:] { 418 if otherLabel.ExclusiveScope() == scope { 419 foundOther = true 420 break 421 } 422 } 423 if foundOther { 424 continue 425 } 426 } 427 validLabels = append(validLabels, label) 428 } 429 430 return validLabels 431 } 432 433 // ReplaceIssueLabels removes all current labels and add new labels to the issue. 434 // Triggers appropriate WebHooks, if any. 435 func ReplaceIssueLabels(ctx context.Context, issue *Issue, labels []*Label, doer *user_model.User) (err error) { 436 ctx, committer, err := db.TxContext(ctx) 437 if err != nil { 438 return err 439 } 440 defer committer.Close() 441 442 if err = issue.LoadRepo(ctx); err != nil { 443 return err 444 } 445 446 if err = issue.LoadLabels(ctx); err != nil { 447 return err 448 } 449 450 labels = RemoveDuplicateExclusiveLabels(labels) 451 452 sort.Sort(labelSorter(labels)) 453 sort.Sort(labelSorter(issue.Labels)) 454 455 var toAdd, toRemove []*Label 456 457 addIndex, removeIndex := 0, 0 458 for addIndex < len(labels) && removeIndex < len(issue.Labels) { 459 addLabel := labels[addIndex] 460 removeLabel := issue.Labels[removeIndex] 461 if addLabel.ID == removeLabel.ID { 462 // Silently drop invalid labels 463 if removeLabel.RepoID != issue.RepoID && removeLabel.OrgID != issue.Repo.OwnerID { 464 toRemove = append(toRemove, removeLabel) 465 } 466 467 addIndex++ 468 removeIndex++ 469 } else if addLabel.ID < removeLabel.ID { 470 // Only add if the label is valid 471 if addLabel.RepoID == issue.RepoID || addLabel.OrgID == issue.Repo.OwnerID { 472 toAdd = append(toAdd, addLabel) 473 } 474 addIndex++ 475 } else { 476 toRemove = append(toRemove, removeLabel) 477 removeIndex++ 478 } 479 } 480 toAdd = append(toAdd, labels[addIndex:]...) 481 toRemove = append(toRemove, issue.Labels[removeIndex:]...) 482 483 if len(toAdd) > 0 { 484 if err = newIssueLabels(ctx, issue, toAdd, doer); err != nil { 485 return fmt.Errorf("addLabels: %w", err) 486 } 487 } 488 489 for _, l := range toRemove { 490 if err = deleteIssueLabel(ctx, issue, l, doer); err != nil { 491 return fmt.Errorf("removeLabel: %w", err) 492 } 493 } 494 495 issue.Labels = nil 496 if err = issue.LoadLabels(ctx); err != nil { 497 return err 498 } 499 500 return committer.Commit() 501 }