loading up the forgejo repo on tangled to test page performance
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Some refactors for issues stats (#24793)

This PR

- [x] Move some functions from `issues.go` to `issue_stats.go` and
`issue_label.go`
- [x] Remove duplicated issue options `UserIssueStatsOption` to keep
only one `IssuesOptions`

authored by

Lunny Xiao and committed by
GitHub
38cf43d0 c757765a

+948 -948
-192
models/issues/issue.go
··· 8 8 "context" 9 9 "fmt" 10 10 "regexp" 11 - "sort" 12 11 13 12 "code.gitea.io/gitea/models/db" 14 - access_model "code.gitea.io/gitea/models/perm/access" 15 13 project_model "code.gitea.io/gitea/models/project" 16 14 repo_model "code.gitea.io/gitea/models/repo" 17 15 user_model "code.gitea.io/gitea/models/user" ··· 210 208 } 211 209 pr.Issue = issue 212 210 return pr, err 213 - } 214 - 215 - // LoadLabels loads labels 216 - func (issue *Issue) LoadLabels(ctx context.Context) (err error) { 217 - if issue.Labels == nil && issue.ID != 0 { 218 - issue.Labels, err = GetLabelsByIssueID(ctx, issue.ID) 219 - if err != nil { 220 - return fmt.Errorf("getLabelsByIssueID [%d]: %w", issue.ID, err) 221 - } 222 - } 223 - return nil 224 211 } 225 212 226 213 // LoadPoster loads poster ··· 459 446 return issue.OriginalAuthorID == 0 && issue.PosterID == uid 460 447 } 461 448 462 - func (issue *Issue) getLabels(ctx context.Context) (err error) { 463 - if len(issue.Labels) > 0 { 464 - return nil 465 - } 466 - 467 - issue.Labels, err = GetLabelsByIssueID(ctx, issue.ID) 468 - if err != nil { 469 - return fmt.Errorf("getLabelsByIssueID: %w", err) 470 - } 471 - return nil 472 - } 473 - 474 - func clearIssueLabels(ctx context.Context, issue *Issue, doer *user_model.User) (err error) { 475 - if err = issue.getLabels(ctx); err != nil { 476 - return fmt.Errorf("getLabels: %w", err) 477 - } 478 - 479 - for i := range issue.Labels { 480 - if err = deleteIssueLabel(ctx, issue, issue.Labels[i], doer); err != nil { 481 - return fmt.Errorf("removeLabel: %w", err) 482 - } 483 - } 484 - 485 - return nil 486 - } 487 - 488 - // ClearIssueLabels removes all issue labels as the given user. 489 - // Triggers appropriate WebHooks, if any. 490 - func ClearIssueLabels(issue *Issue, doer *user_model.User) (err error) { 491 - ctx, committer, err := db.TxContext(db.DefaultContext) 492 - if err != nil { 493 - return err 494 - } 495 - defer committer.Close() 496 - 497 - if err := issue.LoadRepo(ctx); err != nil { 498 - return err 499 - } else if err = issue.LoadPullRequest(ctx); err != nil { 500 - return err 501 - } 502 - 503 - perm, err := access_model.GetUserRepoPermission(ctx, issue.Repo, doer) 504 - if err != nil { 505 - return err 506 - } 507 - if !perm.CanWriteIssuesOrPulls(issue.IsPull) { 508 - return ErrRepoLabelNotExist{} 509 - } 510 - 511 - if err = clearIssueLabels(ctx, issue, doer); err != nil { 512 - return err 513 - } 514 - 515 - if err = committer.Commit(); err != nil { 516 - return fmt.Errorf("Commit: %w", err) 517 - } 518 - 519 - return nil 520 - } 521 - 522 - type labelSorter []*Label 523 - 524 - func (ts labelSorter) Len() int { 525 - return len([]*Label(ts)) 526 - } 527 - 528 - func (ts labelSorter) Less(i, j int) bool { 529 - return []*Label(ts)[i].ID < []*Label(ts)[j].ID 530 - } 531 - 532 - func (ts labelSorter) Swap(i, j int) { 533 - []*Label(ts)[i], []*Label(ts)[j] = []*Label(ts)[j], []*Label(ts)[i] 534 - } 535 - 536 - // Ensure only one label of a given scope exists, with labels at the end of the 537 - // array getting preference over earlier ones. 538 - func RemoveDuplicateExclusiveLabels(labels []*Label) []*Label { 539 - validLabels := make([]*Label, 0, len(labels)) 540 - 541 - for i, label := range labels { 542 - scope := label.ExclusiveScope() 543 - if scope != "" { 544 - foundOther := false 545 - for _, otherLabel := range labels[i+1:] { 546 - if otherLabel.ExclusiveScope() == scope { 547 - foundOther = true 548 - break 549 - } 550 - } 551 - if foundOther { 552 - continue 553 - } 554 - } 555 - validLabels = append(validLabels, label) 556 - } 557 - 558 - return validLabels 559 - } 560 - 561 - // ReplaceIssueLabels removes all current labels and add new labels to the issue. 562 - // Triggers appropriate WebHooks, if any. 563 - func ReplaceIssueLabels(issue *Issue, labels []*Label, doer *user_model.User) (err error) { 564 - ctx, committer, err := db.TxContext(db.DefaultContext) 565 - if err != nil { 566 - return err 567 - } 568 - defer committer.Close() 569 - 570 - if err = issue.LoadRepo(ctx); err != nil { 571 - return err 572 - } 573 - 574 - if err = issue.LoadLabels(ctx); err != nil { 575 - return err 576 - } 577 - 578 - labels = RemoveDuplicateExclusiveLabels(labels) 579 - 580 - sort.Sort(labelSorter(labels)) 581 - sort.Sort(labelSorter(issue.Labels)) 582 - 583 - var toAdd, toRemove []*Label 584 - 585 - addIndex, removeIndex := 0, 0 586 - for addIndex < len(labels) && removeIndex < len(issue.Labels) { 587 - addLabel := labels[addIndex] 588 - removeLabel := issue.Labels[removeIndex] 589 - if addLabel.ID == removeLabel.ID { 590 - // Silently drop invalid labels 591 - if removeLabel.RepoID != issue.RepoID && removeLabel.OrgID != issue.Repo.OwnerID { 592 - toRemove = append(toRemove, removeLabel) 593 - } 594 - 595 - addIndex++ 596 - removeIndex++ 597 - } else if addLabel.ID < removeLabel.ID { 598 - // Only add if the label is valid 599 - if addLabel.RepoID == issue.RepoID || addLabel.OrgID == issue.Repo.OwnerID { 600 - toAdd = append(toAdd, addLabel) 601 - } 602 - addIndex++ 603 - } else { 604 - toRemove = append(toRemove, removeLabel) 605 - removeIndex++ 606 - } 607 - } 608 - toAdd = append(toAdd, labels[addIndex:]...) 609 - toRemove = append(toRemove, issue.Labels[removeIndex:]...) 610 - 611 - if len(toAdd) > 0 { 612 - if err = newIssueLabels(ctx, issue, toAdd, doer); err != nil { 613 - return fmt.Errorf("addLabels: %w", err) 614 - } 615 - } 616 - 617 - for _, l := range toRemove { 618 - if err = deleteIssueLabel(ctx, issue, l, doer); err != nil { 619 - return fmt.Errorf("removeLabel: %w", err) 620 - } 621 - } 622 - 623 - issue.Labels = nil 624 - if err = issue.LoadLabels(ctx); err != nil { 625 - return err 626 - } 627 - 628 - return committer.Commit() 629 - } 630 - 631 449 // GetTasks returns the amount of tasks in the issues content 632 450 func (issue *Issue) GetTasks() int { 633 451 return len(issueTasksPat.FindAllStringIndex(issue.Content, -1)) ··· 861 679 862 680 // GetExternalID ExternalUserRemappable interface 863 681 func (issue *Issue) GetExternalID() int64 { return issue.OriginalAuthorID } 864 - 865 - // CountOrphanedIssues count issues without a repo 866 - func CountOrphanedIssues(ctx context.Context) (int64, error) { 867 - return db.GetEngine(ctx). 868 - Table("issue"). 869 - Join("LEFT", "repository", "issue.repo_id=repository.id"). 870 - Where(builder.IsNull{"repository.id"}). 871 - Select("COUNT(`issue`.`id`)"). 872 - Count() 873 - } 874 682 875 683 // HasOriginalAuthor returns if an issue was migrated and has an original author. 876 684 func (issue *Issue) HasOriginalAuthor() bool {
+490
models/issues/issue_label.go
··· 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 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 + return updateLabelCols(ctx, label, "num_issues", "num_closed_issue") 58 + } 59 + 60 + // Remove all issue labels in the given exclusive scope 61 + func RemoveDuplicateExclusiveIssueLabels(ctx context.Context, issue *Issue, label *Label, doer *user_model.User) (err error) { 62 + scope := label.ExclusiveScope() 63 + if scope == "" { 64 + return nil 65 + } 66 + 67 + var toRemove []*Label 68 + for _, issueLabel := range issue.Labels { 69 + if label.ID != issueLabel.ID && issueLabel.ExclusiveScope() == scope { 70 + toRemove = append(toRemove, issueLabel) 71 + } 72 + } 73 + 74 + for _, issueLabel := range toRemove { 75 + if err = deleteIssueLabel(ctx, issue, issueLabel, doer); err != nil { 76 + return err 77 + } 78 + } 79 + 80 + return nil 81 + } 82 + 83 + // NewIssueLabel creates a new issue-label relation. 84 + func NewIssueLabel(issue *Issue, label *Label, doer *user_model.User) (err error) { 85 + if HasIssueLabel(db.DefaultContext, issue.ID, label.ID) { 86 + return nil 87 + } 88 + 89 + ctx, committer, err := db.TxContext(db.DefaultContext) 90 + if err != nil { 91 + return err 92 + } 93 + defer committer.Close() 94 + 95 + if err = issue.LoadRepo(ctx); err != nil { 96 + return err 97 + } 98 + 99 + // Do NOT add invalid labels 100 + if issue.RepoID != label.RepoID && issue.Repo.OwnerID != label.OrgID { 101 + return nil 102 + } 103 + 104 + if err = RemoveDuplicateExclusiveIssueLabels(ctx, issue, label, doer); err != nil { 105 + return nil 106 + } 107 + 108 + if err = newIssueLabel(ctx, issue, label, doer); err != nil { 109 + return err 110 + } 111 + 112 + issue.Labels = nil 113 + if err = issue.LoadLabels(ctx); err != nil { 114 + return err 115 + } 116 + 117 + return committer.Commit() 118 + } 119 + 120 + // newIssueLabels add labels to an issue. It will check if the labels are valid for the issue 121 + func newIssueLabels(ctx context.Context, issue *Issue, labels []*Label, doer *user_model.User) (err error) { 122 + if err = issue.LoadRepo(ctx); err != nil { 123 + return err 124 + } 125 + for _, l := range labels { 126 + // Don't add already present labels and invalid labels 127 + if HasIssueLabel(ctx, issue.ID, l.ID) || 128 + (l.RepoID != issue.RepoID && l.OrgID != issue.Repo.OwnerID) { 129 + continue 130 + } 131 + 132 + if err = newIssueLabel(ctx, issue, l, doer); err != nil { 133 + return fmt.Errorf("newIssueLabel: %w", err) 134 + } 135 + } 136 + 137 + return nil 138 + } 139 + 140 + // NewIssueLabels creates a list of issue-label relations. 141 + func NewIssueLabels(issue *Issue, labels []*Label, doer *user_model.User) (err error) { 142 + ctx, committer, err := db.TxContext(db.DefaultContext) 143 + if err != nil { 144 + return err 145 + } 146 + defer committer.Close() 147 + 148 + if err = newIssueLabels(ctx, issue, labels, doer); err != nil { 149 + return err 150 + } 151 + 152 + issue.Labels = nil 153 + if err = issue.LoadLabels(ctx); err != nil { 154 + return err 155 + } 156 + 157 + return committer.Commit() 158 + } 159 + 160 + func deleteIssueLabel(ctx context.Context, issue *Issue, label *Label, doer *user_model.User) (err error) { 161 + if count, err := db.DeleteByBean(ctx, &IssueLabel{ 162 + IssueID: issue.ID, 163 + LabelID: label.ID, 164 + }); err != nil { 165 + return err 166 + } else if count == 0 { 167 + return nil 168 + } 169 + 170 + if err = issue.LoadRepo(ctx); err != nil { 171 + return 172 + } 173 + 174 + opts := &CreateCommentOptions{ 175 + Type: CommentTypeLabel, 176 + Doer: doer, 177 + Repo: issue.Repo, 178 + Issue: issue, 179 + Label: label, 180 + } 181 + if _, err = CreateComment(ctx, opts); err != nil { 182 + return err 183 + } 184 + 185 + return updateLabelCols(ctx, label, "num_issues", "num_closed_issue") 186 + } 187 + 188 + // DeleteIssueLabel deletes issue-label relation. 189 + func DeleteIssueLabel(ctx context.Context, issue *Issue, label *Label, doer *user_model.User) error { 190 + if err := deleteIssueLabel(ctx, issue, label, doer); err != nil { 191 + return err 192 + } 193 + 194 + issue.Labels = nil 195 + return issue.LoadLabels(ctx) 196 + } 197 + 198 + // DeleteLabelsByRepoID deletes labels of some repository 199 + func DeleteLabelsByRepoID(ctx context.Context, repoID int64) error { 200 + deleteCond := builder.Select("id").From("label").Where(builder.Eq{"label.repo_id": repoID}) 201 + 202 + if _, err := db.GetEngine(ctx).In("label_id", deleteCond). 203 + Delete(&IssueLabel{}); err != nil { 204 + return err 205 + } 206 + 207 + _, err := db.DeleteByBean(ctx, &Label{RepoID: repoID}) 208 + return err 209 + } 210 + 211 + // CountOrphanedLabels return count of labels witch are broken and not accessible via ui anymore 212 + func CountOrphanedLabels(ctx context.Context) (int64, error) { 213 + noref, err := db.GetEngine(ctx).Table("label").Where("repo_id=? AND org_id=?", 0, 0).Count() 214 + if err != nil { 215 + return 0, err 216 + } 217 + 218 + norepo, err := db.GetEngine(ctx).Table("label"). 219 + Where(builder.And( 220 + builder.Gt{"repo_id": 0}, 221 + builder.NotIn("repo_id", builder.Select("id").From("`repository`")), 222 + )). 223 + Count() 224 + if err != nil { 225 + return 0, err 226 + } 227 + 228 + noorg, err := db.GetEngine(ctx).Table("label"). 229 + Where(builder.And( 230 + builder.Gt{"org_id": 0}, 231 + builder.NotIn("org_id", builder.Select("id").From("`user`")), 232 + )). 233 + Count() 234 + if err != nil { 235 + return 0, err 236 + } 237 + 238 + return noref + norepo + noorg, nil 239 + } 240 + 241 + // DeleteOrphanedLabels delete labels witch are broken and not accessible via ui anymore 242 + func DeleteOrphanedLabels(ctx context.Context) error { 243 + // delete labels with no reference 244 + if _, err := db.GetEngine(ctx).Table("label").Where("repo_id=? AND org_id=?", 0, 0).Delete(new(Label)); err != nil { 245 + return err 246 + } 247 + 248 + // delete labels with none existing repos 249 + if _, err := db.GetEngine(ctx). 250 + Where(builder.And( 251 + builder.Gt{"repo_id": 0}, 252 + builder.NotIn("repo_id", builder.Select("id").From("`repository`")), 253 + )). 254 + Delete(Label{}); err != nil { 255 + return err 256 + } 257 + 258 + // delete labels with none existing orgs 259 + if _, err := db.GetEngine(ctx). 260 + Where(builder.And( 261 + builder.Gt{"org_id": 0}, 262 + builder.NotIn("org_id", builder.Select("id").From("`user`")), 263 + )). 264 + Delete(Label{}); err != nil { 265 + return err 266 + } 267 + 268 + return nil 269 + } 270 + 271 + // CountOrphanedIssueLabels return count of IssueLabels witch have no label behind anymore 272 + func CountOrphanedIssueLabels(ctx context.Context) (int64, error) { 273 + return db.GetEngine(ctx).Table("issue_label"). 274 + NotIn("label_id", builder.Select("id").From("label")). 275 + Count() 276 + } 277 + 278 + // DeleteOrphanedIssueLabels delete IssueLabels witch have no label behind anymore 279 + func DeleteOrphanedIssueLabels(ctx context.Context) error { 280 + _, err := db.GetEngine(ctx). 281 + NotIn("label_id", builder.Select("id").From("label")). 282 + Delete(IssueLabel{}) 283 + return err 284 + } 285 + 286 + // CountIssueLabelWithOutsideLabels count label comments with outside label 287 + func CountIssueLabelWithOutsideLabels(ctx context.Context) (int64, error) { 288 + 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)")). 289 + Table("issue_label"). 290 + Join("inner", "label", "issue_label.label_id = label.id "). 291 + Join("inner", "issue", "issue.id = issue_label.issue_id "). 292 + Join("inner", "repository", "issue.repo_id = repository.id"). 293 + Count(new(IssueLabel)) 294 + } 295 + 296 + // FixIssueLabelWithOutsideLabels fix label comments with outside label 297 + func FixIssueLabelWithOutsideLabels(ctx context.Context) (int64, error) { 298 + res, err := db.GetEngine(ctx).Exec(`DELETE FROM issue_label WHERE issue_label.id IN ( 299 + SELECT il_too.id FROM ( 300 + SELECT il_too_too.id 301 + FROM issue_label AS il_too_too 302 + INNER JOIN label ON il_too_too.label_id = label.id 303 + INNER JOIN issue on issue.id = il_too_too.issue_id 304 + INNER JOIN repository on repository.id = issue.repo_id 305 + WHERE 306 + (label.org_id = 0 AND issue.repo_id != label.repo_id) OR (label.repo_id = 0 AND label.org_id != repository.owner_id) 307 + ) AS il_too )`) 308 + if err != nil { 309 + return 0, err 310 + } 311 + 312 + return res.RowsAffected() 313 + } 314 + 315 + // LoadLabels loads labels 316 + func (issue *Issue) LoadLabels(ctx context.Context) (err error) { 317 + if issue.Labels == nil && issue.ID != 0 { 318 + issue.Labels, err = GetLabelsByIssueID(ctx, issue.ID) 319 + if err != nil { 320 + return fmt.Errorf("getLabelsByIssueID [%d]: %w", issue.ID, err) 321 + } 322 + } 323 + return nil 324 + } 325 + 326 + // GetLabelsByIssueID returns all labels that belong to given issue by ID. 327 + func GetLabelsByIssueID(ctx context.Context, issueID int64) ([]*Label, error) { 328 + var labels []*Label 329 + return labels, db.GetEngine(ctx).Where("issue_label.issue_id = ?", issueID). 330 + Join("LEFT", "issue_label", "issue_label.label_id = label.id"). 331 + Asc("label.name"). 332 + Find(&labels) 333 + } 334 + 335 + func clearIssueLabels(ctx context.Context, issue *Issue, doer *user_model.User) (err error) { 336 + if err = issue.LoadLabels(ctx); err != nil { 337 + return fmt.Errorf("getLabels: %w", err) 338 + } 339 + 340 + for i := range issue.Labels { 341 + if err = deleteIssueLabel(ctx, issue, issue.Labels[i], doer); err != nil { 342 + return fmt.Errorf("removeLabel: %w", err) 343 + } 344 + } 345 + 346 + return nil 347 + } 348 + 349 + // ClearIssueLabels removes all issue labels as the given user. 350 + // Triggers appropriate WebHooks, if any. 351 + func ClearIssueLabels(issue *Issue, doer *user_model.User) (err error) { 352 + ctx, committer, err := db.TxContext(db.DefaultContext) 353 + if err != nil { 354 + return err 355 + } 356 + defer committer.Close() 357 + 358 + if err := issue.LoadRepo(ctx); err != nil { 359 + return err 360 + } else if err = issue.LoadPullRequest(ctx); err != nil { 361 + return err 362 + } 363 + 364 + perm, err := access_model.GetUserRepoPermission(ctx, issue.Repo, doer) 365 + if err != nil { 366 + return err 367 + } 368 + if !perm.CanWriteIssuesOrPulls(issue.IsPull) { 369 + return ErrRepoLabelNotExist{} 370 + } 371 + 372 + if err = clearIssueLabels(ctx, issue, doer); err != nil { 373 + return err 374 + } 375 + 376 + if err = committer.Commit(); err != nil { 377 + return fmt.Errorf("Commit: %w", err) 378 + } 379 + 380 + return nil 381 + } 382 + 383 + type labelSorter []*Label 384 + 385 + func (ts labelSorter) Len() int { 386 + return len([]*Label(ts)) 387 + } 388 + 389 + func (ts labelSorter) Less(i, j int) bool { 390 + return []*Label(ts)[i].ID < []*Label(ts)[j].ID 391 + } 392 + 393 + func (ts labelSorter) Swap(i, j int) { 394 + []*Label(ts)[i], []*Label(ts)[j] = []*Label(ts)[j], []*Label(ts)[i] 395 + } 396 + 397 + // Ensure only one label of a given scope exists, with labels at the end of the 398 + // array getting preference over earlier ones. 399 + func RemoveDuplicateExclusiveLabels(labels []*Label) []*Label { 400 + validLabels := make([]*Label, 0, len(labels)) 401 + 402 + for i, label := range labels { 403 + scope := label.ExclusiveScope() 404 + if scope != "" { 405 + foundOther := false 406 + for _, otherLabel := range labels[i+1:] { 407 + if otherLabel.ExclusiveScope() == scope { 408 + foundOther = true 409 + break 410 + } 411 + } 412 + if foundOther { 413 + continue 414 + } 415 + } 416 + validLabels = append(validLabels, label) 417 + } 418 + 419 + return validLabels 420 + } 421 + 422 + // ReplaceIssueLabels removes all current labels and add new labels to the issue. 423 + // Triggers appropriate WebHooks, if any. 424 + func ReplaceIssueLabels(issue *Issue, labels []*Label, doer *user_model.User) (err error) { 425 + ctx, committer, err := db.TxContext(db.DefaultContext) 426 + if err != nil { 427 + return err 428 + } 429 + defer committer.Close() 430 + 431 + if err = issue.LoadRepo(ctx); err != nil { 432 + return err 433 + } 434 + 435 + if err = issue.LoadLabels(ctx); err != nil { 436 + return err 437 + } 438 + 439 + labels = RemoveDuplicateExclusiveLabels(labels) 440 + 441 + sort.Sort(labelSorter(labels)) 442 + sort.Sort(labelSorter(issue.Labels)) 443 + 444 + var toAdd, toRemove []*Label 445 + 446 + addIndex, removeIndex := 0, 0 447 + for addIndex < len(labels) && removeIndex < len(issue.Labels) { 448 + addLabel := labels[addIndex] 449 + removeLabel := issue.Labels[removeIndex] 450 + if addLabel.ID == removeLabel.ID { 451 + // Silently drop invalid labels 452 + if removeLabel.RepoID != issue.RepoID && removeLabel.OrgID != issue.Repo.OwnerID { 453 + toRemove = append(toRemove, removeLabel) 454 + } 455 + 456 + addIndex++ 457 + removeIndex++ 458 + } else if addLabel.ID < removeLabel.ID { 459 + // Only add if the label is valid 460 + if addLabel.RepoID == issue.RepoID || addLabel.OrgID == issue.Repo.OwnerID { 461 + toAdd = append(toAdd, addLabel) 462 + } 463 + addIndex++ 464 + } else { 465 + toRemove = append(toRemove, removeLabel) 466 + removeIndex++ 467 + } 468 + } 469 + toAdd = append(toAdd, labels[addIndex:]...) 470 + toRemove = append(toRemove, issue.Labels[removeIndex:]...) 471 + 472 + if len(toAdd) > 0 { 473 + if err = newIssueLabels(ctx, issue, toAdd, doer); err != nil { 474 + return fmt.Errorf("addLabels: %w", err) 475 + } 476 + } 477 + 478 + for _, l := range toRemove { 479 + if err = deleteIssueLabel(ctx, issue, l, doer); err != nil { 480 + return fmt.Errorf("removeLabel: %w", err) 481 + } 482 + } 483 + 484 + issue.Labels = nil 485 + if err = issue.LoadLabels(ctx); err != nil { 486 + return err 487 + } 488 + 489 + return committer.Commit() 490 + }
+16 -367
models/issues/issue_search.go
··· 22 22 // IssuesOptions represents options of an issue. 23 23 type IssuesOptions struct { //nolint 24 24 db.ListOptions 25 - RepoID int64 // overwrites RepoCond if not 0 25 + RepoIDs []int64 // overwrites RepoCond if the length is not 0 26 26 RepoCond builder.Cond 27 27 AssigneeID int64 28 28 PosterID int64 ··· 155 155 return sess 156 156 } 157 157 158 + func applyRepoConditions(sess *xorm.Session, opts *IssuesOptions) *xorm.Session { 159 + if len(opts.RepoIDs) == 1 { 160 + opts.RepoCond = builder.Eq{"issue.repo_id": opts.RepoIDs[0]} 161 + } else if len(opts.RepoIDs) > 1 { 162 + opts.RepoCond = builder.In("issue.repo_id", opts.RepoIDs) 163 + } 164 + if opts.RepoCond != nil { 165 + sess.And(opts.RepoCond) 166 + } 167 + return sess 168 + } 169 + 158 170 func applyConditions(sess *xorm.Session, opts *IssuesOptions) *xorm.Session { 159 171 if len(opts.IssueIDs) > 0 { 160 172 sess.In("issue.id", opts.IssueIDs) 161 173 } 162 174 163 - if opts.RepoID != 0 { 164 - opts.RepoCond = builder.Eq{"issue.repo_id": opts.RepoID} 165 - } 166 - if opts.RepoCond != nil { 167 - sess.And(opts.RepoCond) 168 - } 175 + applyRepoConditions(sess, opts) 169 176 170 177 if !opts.IsClosed.IsNone() { 171 178 sess.And("issue.is_closed=?", opts.IsClosed.IsTrue()) ··· 400 407 ) 401 408 } 402 409 403 - // CountIssuesByRepo map from repoID to number of issues matching the options 404 - func CountIssuesByRepo(ctx context.Context, opts *IssuesOptions) (map[int64]int64, error) { 405 - sess := db.GetEngine(ctx). 406 - Join("INNER", "repository", "`issue`.repo_id = `repository`.id") 407 - 408 - applyConditions(sess, opts) 409 - 410 - countsSlice := make([]*struct { 411 - RepoID int64 412 - Count int64 413 - }, 0, 10) 414 - if err := sess.GroupBy("issue.repo_id"). 415 - Select("issue.repo_id AS repo_id, COUNT(*) AS count"). 416 - Table("issue"). 417 - Find(&countsSlice); err != nil { 418 - return nil, fmt.Errorf("unable to CountIssuesByRepo: %w", err) 419 - } 420 - 421 - countMap := make(map[int64]int64, len(countsSlice)) 422 - for _, c := range countsSlice { 423 - countMap[c.RepoID] = c.Count 424 - } 425 - return countMap, nil 426 - } 427 - 428 410 // GetRepoIDsForIssuesOptions find all repo ids for the given options 429 411 func GetRepoIDsForIssuesOptions(opts *IssuesOptions, user *user_model.User) ([]int64, error) { 430 412 repoIDs := make([]int64, 0, 5) ··· 453 435 applyConditions(sess, opts) 454 436 applySorts(sess, opts.SortType, opts.PriorityRepoID) 455 437 456 - issues := make([]*Issue, 0, opts.ListOptions.PageSize) 438 + issues := make(IssueList, 0, opts.ListOptions.PageSize) 457 439 if err := sess.Find(&issues); err != nil { 458 440 return nil, fmt.Errorf("unable to query Issues: %w", err) 459 441 } 460 442 461 - if err := IssueList(issues).LoadAttributes(); err != nil { 443 + if err := issues.LoadAttributes(); err != nil { 462 444 return nil, fmt.Errorf("unable to LoadAttributes for Issues: %w", err) 463 445 } 464 446 465 447 return issues, nil 466 - } 467 - 468 - // CountIssues number return of issues by given conditions. 469 - func CountIssues(ctx context.Context, opts *IssuesOptions) (int64, error) { 470 - sess := db.GetEngine(ctx). 471 - Select("COUNT(issue.id) AS count"). 472 - Table("issue"). 473 - Join("INNER", "repository", "`issue`.repo_id = `repository`.id") 474 - applyConditions(sess, opts) 475 - 476 - return sess.Count() 477 - } 478 - 479 - // IssueStats represents issue statistic information. 480 - type IssueStats struct { 481 - OpenCount, ClosedCount int64 482 - YourRepositoriesCount int64 483 - AssignCount int64 484 - CreateCount int64 485 - MentionCount int64 486 - ReviewRequestedCount int64 487 - ReviewedCount int64 488 - } 489 - 490 - // Filter modes. 491 - const ( 492 - FilterModeAll = iota 493 - FilterModeAssign 494 - FilterModeCreate 495 - FilterModeMention 496 - FilterModeReviewRequested 497 - FilterModeReviewed 498 - FilterModeYourRepositories 499 - ) 500 - 501 - const ( 502 - // MaxQueryParameters represents the max query parameters 503 - // When queries are broken down in parts because of the number 504 - // of parameters, attempt to break by this amount 505 - MaxQueryParameters = 300 506 - ) 507 - 508 - // GetIssueStats returns issue statistic information by given conditions. 509 - func GetIssueStats(opts *IssuesOptions) (*IssueStats, error) { 510 - if len(opts.IssueIDs) <= MaxQueryParameters { 511 - return getIssueStatsChunk(opts, opts.IssueIDs) 512 - } 513 - 514 - // If too long a list of IDs is provided, we get the statistics in 515 - // smaller chunks and get accumulates. Note: this could potentially 516 - // get us invalid results. The alternative is to insert the list of 517 - // ids in a temporary table and join from them. 518 - accum := &IssueStats{} 519 - for i := 0; i < len(opts.IssueIDs); { 520 - chunk := i + MaxQueryParameters 521 - if chunk > len(opts.IssueIDs) { 522 - chunk = len(opts.IssueIDs) 523 - } 524 - stats, err := getIssueStatsChunk(opts, opts.IssueIDs[i:chunk]) 525 - if err != nil { 526 - return nil, err 527 - } 528 - accum.OpenCount += stats.OpenCount 529 - accum.ClosedCount += stats.ClosedCount 530 - accum.YourRepositoriesCount += stats.YourRepositoriesCount 531 - accum.AssignCount += stats.AssignCount 532 - accum.CreateCount += stats.CreateCount 533 - accum.OpenCount += stats.MentionCount 534 - accum.ReviewRequestedCount += stats.ReviewRequestedCount 535 - accum.ReviewedCount += stats.ReviewedCount 536 - i = chunk 537 - } 538 - return accum, nil 539 - } 540 - 541 - func getIssueStatsChunk(opts *IssuesOptions, issueIDs []int64) (*IssueStats, error) { 542 - stats := &IssueStats{} 543 - 544 - countSession := func(opts *IssuesOptions, issueIDs []int64) *xorm.Session { 545 - sess := db.GetEngine(db.DefaultContext). 546 - Where("issue.repo_id = ?", opts.RepoID) 547 - 548 - if len(issueIDs) > 0 { 549 - sess.In("issue.id", issueIDs) 550 - } 551 - 552 - applyLabelsCondition(sess, opts) 553 - 554 - applyMilestoneCondition(sess, opts) 555 - 556 - if opts.ProjectID > 0 { 557 - sess.Join("INNER", "project_issue", "issue.id = project_issue.issue_id"). 558 - And("project_issue.project_id=?", opts.ProjectID) 559 - } 560 - 561 - if opts.AssigneeID > 0 { 562 - applyAssigneeCondition(sess, opts.AssigneeID) 563 - } else if opts.AssigneeID == db.NoConditionID { 564 - sess.Where("id NOT IN (SELECT issue_id FROM issue_assignees)") 565 - } 566 - 567 - if opts.PosterID > 0 { 568 - applyPosterCondition(sess, opts.PosterID) 569 - } 570 - 571 - if opts.MentionedID > 0 { 572 - applyMentionedCondition(sess, opts.MentionedID) 573 - } 574 - 575 - if opts.ReviewRequestedID > 0 { 576 - applyReviewRequestedCondition(sess, opts.ReviewRequestedID) 577 - } 578 - 579 - if opts.ReviewedID > 0 { 580 - applyReviewedCondition(sess, opts.ReviewedID) 581 - } 582 - 583 - switch opts.IsPull { 584 - case util.OptionalBoolTrue: 585 - sess.And("issue.is_pull=?", true) 586 - case util.OptionalBoolFalse: 587 - sess.And("issue.is_pull=?", false) 588 - } 589 - 590 - return sess 591 - } 592 - 593 - var err error 594 - stats.OpenCount, err = countSession(opts, issueIDs). 595 - And("issue.is_closed = ?", false). 596 - Count(new(Issue)) 597 - if err != nil { 598 - return stats, err 599 - } 600 - stats.ClosedCount, err = countSession(opts, issueIDs). 601 - And("issue.is_closed = ?", true). 602 - Count(new(Issue)) 603 - return stats, err 604 - } 605 - 606 - // UserIssueStatsOptions contains parameters accepted by GetUserIssueStats. 607 - type UserIssueStatsOptions struct { 608 - UserID int64 609 - RepoIDs []int64 610 - FilterMode int 611 - IsPull bool 612 - IsClosed bool 613 - IssueIDs []int64 614 - IsArchived util.OptionalBool 615 - LabelIDs []int64 616 - RepoCond builder.Cond 617 - Org *organization.Organization 618 - Team *organization.Team 619 - } 620 - 621 - // GetUserIssueStats returns issue statistic information for dashboard by given conditions. 622 - func GetUserIssueStats(opts UserIssueStatsOptions) (*IssueStats, error) { 623 - var err error 624 - stats := &IssueStats{} 625 - 626 - cond := builder.NewCond() 627 - cond = cond.And(builder.Eq{"issue.is_pull": opts.IsPull}) 628 - if len(opts.RepoIDs) > 0 { 629 - cond = cond.And(builder.In("issue.repo_id", opts.RepoIDs)) 630 - } 631 - if len(opts.IssueIDs) > 0 { 632 - cond = cond.And(builder.In("issue.id", opts.IssueIDs)) 633 - } 634 - if opts.RepoCond != nil { 635 - cond = cond.And(opts.RepoCond) 636 - } 637 - 638 - if opts.UserID > 0 { 639 - cond = cond.And(issuePullAccessibleRepoCond("issue.repo_id", opts.UserID, opts.Org, opts.Team, opts.IsPull)) 640 - } 641 - 642 - sess := func(cond builder.Cond) *xorm.Session { 643 - s := db.GetEngine(db.DefaultContext).Where(cond) 644 - if len(opts.LabelIDs) > 0 { 645 - s.Join("INNER", "issue_label", "issue_label.issue_id = issue.id"). 646 - In("issue_label.label_id", opts.LabelIDs) 647 - } 648 - if opts.UserID > 0 || opts.IsArchived != util.OptionalBoolNone { 649 - s.Join("INNER", "repository", "issue.repo_id = repository.id") 650 - if opts.IsArchived != util.OptionalBoolNone { 651 - s.And(builder.Eq{"repository.is_archived": opts.IsArchived.IsTrue()}) 652 - } 653 - } 654 - return s 655 - } 656 - 657 - switch opts.FilterMode { 658 - case FilterModeAll, FilterModeYourRepositories: 659 - stats.OpenCount, err = sess(cond). 660 - And("issue.is_closed = ?", false). 661 - Count(new(Issue)) 662 - if err != nil { 663 - return nil, err 664 - } 665 - stats.ClosedCount, err = sess(cond). 666 - And("issue.is_closed = ?", true). 667 - Count(new(Issue)) 668 - if err != nil { 669 - return nil, err 670 - } 671 - case FilterModeAssign: 672 - stats.OpenCount, err = applyAssigneeCondition(sess(cond), opts.UserID). 673 - And("issue.is_closed = ?", false). 674 - Count(new(Issue)) 675 - if err != nil { 676 - return nil, err 677 - } 678 - stats.ClosedCount, err = applyAssigneeCondition(sess(cond), opts.UserID). 679 - And("issue.is_closed = ?", true). 680 - Count(new(Issue)) 681 - if err != nil { 682 - return nil, err 683 - } 684 - case FilterModeCreate: 685 - stats.OpenCount, err = applyPosterCondition(sess(cond), opts.UserID). 686 - And("issue.is_closed = ?", false). 687 - Count(new(Issue)) 688 - if err != nil { 689 - return nil, err 690 - } 691 - stats.ClosedCount, err = applyPosterCondition(sess(cond), opts.UserID). 692 - And("issue.is_closed = ?", true). 693 - Count(new(Issue)) 694 - if err != nil { 695 - return nil, err 696 - } 697 - case FilterModeMention: 698 - stats.OpenCount, err = applyMentionedCondition(sess(cond), opts.UserID). 699 - And("issue.is_closed = ?", false). 700 - Count(new(Issue)) 701 - if err != nil { 702 - return nil, err 703 - } 704 - stats.ClosedCount, err = applyMentionedCondition(sess(cond), opts.UserID). 705 - And("issue.is_closed = ?", true). 706 - Count(new(Issue)) 707 - if err != nil { 708 - return nil, err 709 - } 710 - case FilterModeReviewRequested: 711 - stats.OpenCount, err = applyReviewRequestedCondition(sess(cond), opts.UserID). 712 - And("issue.is_closed = ?", false). 713 - Count(new(Issue)) 714 - if err != nil { 715 - return nil, err 716 - } 717 - stats.ClosedCount, err = applyReviewRequestedCondition(sess(cond), opts.UserID). 718 - And("issue.is_closed = ?", true). 719 - Count(new(Issue)) 720 - if err != nil { 721 - return nil, err 722 - } 723 - case FilterModeReviewed: 724 - stats.OpenCount, err = applyReviewedCondition(sess(cond), opts.UserID). 725 - And("issue.is_closed = ?", false). 726 - Count(new(Issue)) 727 - if err != nil { 728 - return nil, err 729 - } 730 - stats.ClosedCount, err = applyReviewedCondition(sess(cond), opts.UserID). 731 - And("issue.is_closed = ?", true). 732 - Count(new(Issue)) 733 - if err != nil { 734 - return nil, err 735 - } 736 - } 737 - 738 - cond = cond.And(builder.Eq{"issue.is_closed": opts.IsClosed}) 739 - stats.AssignCount, err = applyAssigneeCondition(sess(cond), opts.UserID).Count(new(Issue)) 740 - if err != nil { 741 - return nil, err 742 - } 743 - 744 - stats.CreateCount, err = applyPosterCondition(sess(cond), opts.UserID).Count(new(Issue)) 745 - if err != nil { 746 - return nil, err 747 - } 748 - 749 - stats.MentionCount, err = applyMentionedCondition(sess(cond), opts.UserID).Count(new(Issue)) 750 - if err != nil { 751 - return nil, err 752 - } 753 - 754 - stats.YourRepositoriesCount, err = sess(cond).Count(new(Issue)) 755 - if err != nil { 756 - return nil, err 757 - } 758 - 759 - stats.ReviewRequestedCount, err = applyReviewRequestedCondition(sess(cond), opts.UserID).Count(new(Issue)) 760 - if err != nil { 761 - return nil, err 762 - } 763 - 764 - stats.ReviewedCount, err = applyReviewedCondition(sess(cond), opts.UserID).Count(new(Issue)) 765 - if err != nil { 766 - return nil, err 767 - } 768 - 769 - return stats, nil 770 - } 771 - 772 - // GetRepoIssueStats returns number of open and closed repository issues by given filter mode. 773 - func GetRepoIssueStats(repoID, uid int64, filterMode int, isPull bool) (numOpen, numClosed int64) { 774 - countSession := func(isClosed, isPull bool, repoID int64) *xorm.Session { 775 - sess := db.GetEngine(db.DefaultContext). 776 - Where("is_closed = ?", isClosed). 777 - And("is_pull = ?", isPull). 778 - And("repo_id = ?", repoID) 779 - 780 - return sess 781 - } 782 - 783 - openCountSession := countSession(false, isPull, repoID) 784 - closedCountSession := countSession(true, isPull, repoID) 785 - 786 - switch filterMode { 787 - case FilterModeAssign: 788 - applyAssigneeCondition(openCountSession, uid) 789 - applyAssigneeCondition(closedCountSession, uid) 790 - case FilterModeCreate: 791 - applyPosterCondition(openCountSession, uid) 792 - applyPosterCondition(closedCountSession, uid) 793 - } 794 - 795 - openResult, _ := openCountSession.Count(new(Issue)) 796 - closedResult, _ := closedCountSession.Count(new(Issue)) 797 - 798 - return openResult, closedResult 799 448 } 800 449 801 450 // SearchIssueIDsByKeyword search issues on database
+383
models/issues/issue_stats.go
··· 1 + // Copyright 2023 The Gitea Authors. All rights reserved. 2 + // SPDX-License-Identifier: MIT 3 + 4 + package issues 5 + 6 + import ( 7 + "context" 8 + "errors" 9 + "fmt" 10 + 11 + "code.gitea.io/gitea/models/db" 12 + "code.gitea.io/gitea/modules/util" 13 + 14 + "xorm.io/builder" 15 + "xorm.io/xorm" 16 + ) 17 + 18 + // IssueStats represents issue statistic information. 19 + type IssueStats struct { 20 + OpenCount, ClosedCount int64 21 + YourRepositoriesCount int64 22 + AssignCount int64 23 + CreateCount int64 24 + MentionCount int64 25 + ReviewRequestedCount int64 26 + ReviewedCount int64 27 + } 28 + 29 + // Filter modes. 30 + const ( 31 + FilterModeAll = iota 32 + FilterModeAssign 33 + FilterModeCreate 34 + FilterModeMention 35 + FilterModeReviewRequested 36 + FilterModeReviewed 37 + FilterModeYourRepositories 38 + ) 39 + 40 + const ( 41 + // MaxQueryParameters represents the max query parameters 42 + // When queries are broken down in parts because of the number 43 + // of parameters, attempt to break by this amount 44 + MaxQueryParameters = 300 45 + ) 46 + 47 + // CountIssuesByRepo map from repoID to number of issues matching the options 48 + func CountIssuesByRepo(ctx context.Context, opts *IssuesOptions) (map[int64]int64, error) { 49 + sess := db.GetEngine(ctx). 50 + Join("INNER", "repository", "`issue`.repo_id = `repository`.id") 51 + 52 + applyConditions(sess, opts) 53 + 54 + countsSlice := make([]*struct { 55 + RepoID int64 56 + Count int64 57 + }, 0, 10) 58 + if err := sess.GroupBy("issue.repo_id"). 59 + Select("issue.repo_id AS repo_id, COUNT(*) AS count"). 60 + Table("issue"). 61 + Find(&countsSlice); err != nil { 62 + return nil, fmt.Errorf("unable to CountIssuesByRepo: %w", err) 63 + } 64 + 65 + countMap := make(map[int64]int64, len(countsSlice)) 66 + for _, c := range countsSlice { 67 + countMap[c.RepoID] = c.Count 68 + } 69 + return countMap, nil 70 + } 71 + 72 + // CountIssues number return of issues by given conditions. 73 + func CountIssues(ctx context.Context, opts *IssuesOptions) (int64, error) { 74 + sess := db.GetEngine(ctx). 75 + Select("COUNT(issue.id) AS count"). 76 + Table("issue"). 77 + Join("INNER", "repository", "`issue`.repo_id = `repository`.id") 78 + applyConditions(sess, opts) 79 + 80 + return sess.Count() 81 + } 82 + 83 + // GetIssueStats returns issue statistic information by given conditions. 84 + func GetIssueStats(opts *IssuesOptions) (*IssueStats, error) { 85 + if len(opts.IssueIDs) <= MaxQueryParameters { 86 + return getIssueStatsChunk(opts, opts.IssueIDs) 87 + } 88 + 89 + // If too long a list of IDs is provided, we get the statistics in 90 + // smaller chunks and get accumulates. Note: this could potentially 91 + // get us invalid results. The alternative is to insert the list of 92 + // ids in a temporary table and join from them. 93 + accum := &IssueStats{} 94 + for i := 0; i < len(opts.IssueIDs); { 95 + chunk := i + MaxQueryParameters 96 + if chunk > len(opts.IssueIDs) { 97 + chunk = len(opts.IssueIDs) 98 + } 99 + stats, err := getIssueStatsChunk(opts, opts.IssueIDs[i:chunk]) 100 + if err != nil { 101 + return nil, err 102 + } 103 + accum.OpenCount += stats.OpenCount 104 + accum.ClosedCount += stats.ClosedCount 105 + accum.YourRepositoriesCount += stats.YourRepositoriesCount 106 + accum.AssignCount += stats.AssignCount 107 + accum.CreateCount += stats.CreateCount 108 + accum.OpenCount += stats.MentionCount 109 + accum.ReviewRequestedCount += stats.ReviewRequestedCount 110 + accum.ReviewedCount += stats.ReviewedCount 111 + i = chunk 112 + } 113 + return accum, nil 114 + } 115 + 116 + func getIssueStatsChunk(opts *IssuesOptions, issueIDs []int64) (*IssueStats, error) { 117 + stats := &IssueStats{} 118 + 119 + countSession := func(opts *IssuesOptions, issueIDs []int64) *xorm.Session { 120 + sess := db.GetEngine(db.DefaultContext). 121 + Join("INNER", "repository", "`issue`.repo_id = `repository`.id") 122 + if len(opts.RepoIDs) > 1 { 123 + sess.In("issue.repo_id", opts.RepoIDs) 124 + } else if len(opts.RepoIDs) == 1 { 125 + sess.And("issue.repo_id = ?", opts.RepoIDs[0]) 126 + } 127 + 128 + if len(issueIDs) > 0 { 129 + sess.In("issue.id", issueIDs) 130 + } 131 + 132 + applyLabelsCondition(sess, opts) 133 + 134 + applyMilestoneCondition(sess, opts) 135 + 136 + if opts.ProjectID > 0 { 137 + sess.Join("INNER", "project_issue", "issue.id = project_issue.issue_id"). 138 + And("project_issue.project_id=?", opts.ProjectID) 139 + } 140 + 141 + if opts.AssigneeID > 0 { 142 + applyAssigneeCondition(sess, opts.AssigneeID) 143 + } else if opts.AssigneeID == db.NoConditionID { 144 + sess.Where("id NOT IN (SELECT issue_id FROM issue_assignees)") 145 + } 146 + 147 + if opts.PosterID > 0 { 148 + applyPosterCondition(sess, opts.PosterID) 149 + } 150 + 151 + if opts.MentionedID > 0 { 152 + applyMentionedCondition(sess, opts.MentionedID) 153 + } 154 + 155 + if opts.ReviewRequestedID > 0 { 156 + applyReviewRequestedCondition(sess, opts.ReviewRequestedID) 157 + } 158 + 159 + if opts.ReviewedID > 0 { 160 + applyReviewedCondition(sess, opts.ReviewedID) 161 + } 162 + 163 + switch opts.IsPull { 164 + case util.OptionalBoolTrue: 165 + sess.And("issue.is_pull=?", true) 166 + case util.OptionalBoolFalse: 167 + sess.And("issue.is_pull=?", false) 168 + } 169 + 170 + return sess 171 + } 172 + 173 + var err error 174 + stats.OpenCount, err = countSession(opts, issueIDs). 175 + And("issue.is_closed = ?", false). 176 + Count(new(Issue)) 177 + if err != nil { 178 + return stats, err 179 + } 180 + stats.ClosedCount, err = countSession(opts, issueIDs). 181 + And("issue.is_closed = ?", true). 182 + Count(new(Issue)) 183 + return stats, err 184 + } 185 + 186 + // GetUserIssueStats returns issue statistic information for dashboard by given conditions. 187 + func GetUserIssueStats(filterMode int, opts IssuesOptions) (*IssueStats, error) { 188 + if opts.User == nil { 189 + return nil, errors.New("issue stats without user") 190 + } 191 + if opts.IsPull.IsNone() { 192 + return nil, errors.New("unaccepted ispull option") 193 + } 194 + 195 + var err error 196 + stats := &IssueStats{} 197 + 198 + cond := builder.NewCond() 199 + 200 + cond = cond.And(builder.Eq{"issue.is_pull": opts.IsPull.IsTrue()}) 201 + 202 + if len(opts.RepoIDs) > 0 { 203 + cond = cond.And(builder.In("issue.repo_id", opts.RepoIDs)) 204 + } 205 + if len(opts.IssueIDs) > 0 { 206 + cond = cond.And(builder.In("issue.id", opts.IssueIDs)) 207 + } 208 + if opts.RepoCond != nil { 209 + cond = cond.And(opts.RepoCond) 210 + } 211 + 212 + if opts.User != nil { 213 + cond = cond.And(issuePullAccessibleRepoCond("issue.repo_id", opts.User.ID, opts.Org, opts.Team, opts.IsPull.IsTrue())) 214 + } 215 + 216 + sess := func(cond builder.Cond) *xorm.Session { 217 + s := db.GetEngine(db.DefaultContext). 218 + Join("INNER", "repository", "`issue`.repo_id = `repository`.id"). 219 + Where(cond) 220 + if len(opts.LabelIDs) > 0 { 221 + s.Join("INNER", "issue_label", "issue_label.issue_id = issue.id"). 222 + In("issue_label.label_id", opts.LabelIDs) 223 + } 224 + 225 + if opts.IsArchived != util.OptionalBoolNone { 226 + s.And(builder.Eq{"repository.is_archived": opts.IsArchived.IsTrue()}) 227 + } 228 + return s 229 + } 230 + 231 + switch filterMode { 232 + case FilterModeAll, FilterModeYourRepositories: 233 + stats.OpenCount, err = sess(cond). 234 + And("issue.is_closed = ?", false). 235 + Count(new(Issue)) 236 + if err != nil { 237 + return nil, err 238 + } 239 + stats.ClosedCount, err = sess(cond). 240 + And("issue.is_closed = ?", true). 241 + Count(new(Issue)) 242 + if err != nil { 243 + return nil, err 244 + } 245 + case FilterModeAssign: 246 + stats.OpenCount, err = applyAssigneeCondition(sess(cond), opts.User.ID). 247 + And("issue.is_closed = ?", false). 248 + Count(new(Issue)) 249 + if err != nil { 250 + return nil, err 251 + } 252 + stats.ClosedCount, err = applyAssigneeCondition(sess(cond), opts.User.ID). 253 + And("issue.is_closed = ?", true). 254 + Count(new(Issue)) 255 + if err != nil { 256 + return nil, err 257 + } 258 + case FilterModeCreate: 259 + stats.OpenCount, err = applyPosterCondition(sess(cond), opts.User.ID). 260 + And("issue.is_closed = ?", false). 261 + Count(new(Issue)) 262 + if err != nil { 263 + return nil, err 264 + } 265 + stats.ClosedCount, err = applyPosterCondition(sess(cond), opts.User.ID). 266 + And("issue.is_closed = ?", true). 267 + Count(new(Issue)) 268 + if err != nil { 269 + return nil, err 270 + } 271 + case FilterModeMention: 272 + stats.OpenCount, err = applyMentionedCondition(sess(cond), opts.User.ID). 273 + And("issue.is_closed = ?", false). 274 + Count(new(Issue)) 275 + if err != nil { 276 + return nil, err 277 + } 278 + stats.ClosedCount, err = applyMentionedCondition(sess(cond), opts.User.ID). 279 + And("issue.is_closed = ?", true). 280 + Count(new(Issue)) 281 + if err != nil { 282 + return nil, err 283 + } 284 + case FilterModeReviewRequested: 285 + stats.OpenCount, err = applyReviewRequestedCondition(sess(cond), opts.User.ID). 286 + And("issue.is_closed = ?", false). 287 + Count(new(Issue)) 288 + if err != nil { 289 + return nil, err 290 + } 291 + stats.ClosedCount, err = applyReviewRequestedCondition(sess(cond), opts.User.ID). 292 + And("issue.is_closed = ?", true). 293 + Count(new(Issue)) 294 + if err != nil { 295 + return nil, err 296 + } 297 + case FilterModeReviewed: 298 + stats.OpenCount, err = applyReviewedCondition(sess(cond), opts.User.ID). 299 + And("issue.is_closed = ?", false). 300 + Count(new(Issue)) 301 + if err != nil { 302 + return nil, err 303 + } 304 + stats.ClosedCount, err = applyReviewedCondition(sess(cond), opts.User.ID). 305 + And("issue.is_closed = ?", true). 306 + Count(new(Issue)) 307 + if err != nil { 308 + return nil, err 309 + } 310 + } 311 + 312 + cond = cond.And(builder.Eq{"issue.is_closed": opts.IsClosed.IsTrue()}) 313 + stats.AssignCount, err = applyAssigneeCondition(sess(cond), opts.User.ID).Count(new(Issue)) 314 + if err != nil { 315 + return nil, err 316 + } 317 + 318 + stats.CreateCount, err = applyPosterCondition(sess(cond), opts.User.ID).Count(new(Issue)) 319 + if err != nil { 320 + return nil, err 321 + } 322 + 323 + stats.MentionCount, err = applyMentionedCondition(sess(cond), opts.User.ID).Count(new(Issue)) 324 + if err != nil { 325 + return nil, err 326 + } 327 + 328 + stats.YourRepositoriesCount, err = sess(cond).Count(new(Issue)) 329 + if err != nil { 330 + return nil, err 331 + } 332 + 333 + stats.ReviewRequestedCount, err = applyReviewRequestedCondition(sess(cond), opts.User.ID).Count(new(Issue)) 334 + if err != nil { 335 + return nil, err 336 + } 337 + 338 + stats.ReviewedCount, err = applyReviewedCondition(sess(cond), opts.User.ID).Count(new(Issue)) 339 + if err != nil { 340 + return nil, err 341 + } 342 + 343 + return stats, nil 344 + } 345 + 346 + // GetRepoIssueStats returns number of open and closed repository issues by given filter mode. 347 + func GetRepoIssueStats(repoID, uid int64, filterMode int, isPull bool) (numOpen, numClosed int64) { 348 + countSession := func(isClosed, isPull bool, repoID int64) *xorm.Session { 349 + sess := db.GetEngine(db.DefaultContext). 350 + Where("is_closed = ?", isClosed). 351 + And("is_pull = ?", isPull). 352 + And("repo_id = ?", repoID) 353 + 354 + return sess 355 + } 356 + 357 + openCountSession := countSession(false, isPull, repoID) 358 + closedCountSession := countSession(true, isPull, repoID) 359 + 360 + switch filterMode { 361 + case FilterModeAssign: 362 + applyAssigneeCondition(openCountSession, uid) 363 + applyAssigneeCondition(closedCountSession, uid) 364 + case FilterModeCreate: 365 + applyPosterCondition(openCountSession, uid) 366 + applyPosterCondition(closedCountSession, uid) 367 + } 368 + 369 + openResult, _ := openCountSession.Count(new(Issue)) 370 + closedResult, _ := closedCountSession.Count(new(Issue)) 371 + 372 + return openResult, closedResult 373 + } 374 + 375 + // CountOrphanedIssues count issues without a repo 376 + func CountOrphanedIssues(ctx context.Context) (int64, error) { 377 + return db.GetEngine(ctx). 378 + Table("issue"). 379 + Join("LEFT", "repository", "issue.repo_id=repository.id"). 380 + Where(builder.IsNull{"repository.id"}). 381 + Select("COUNT(`issue`.`id`)"). 382 + Count() 383 + }
+39 -30
models/issues/issue_test.go
··· 17 17 repo_model "code.gitea.io/gitea/models/repo" 18 18 "code.gitea.io/gitea/models/unittest" 19 19 user_model "code.gitea.io/gitea/models/user" 20 + "code.gitea.io/gitea/modules/util" 20 21 21 22 "github.com/stretchr/testify/assert" 22 23 "xorm.io/builder" ··· 204 205 func TestGetUserIssueStats(t *testing.T) { 205 206 assert.NoError(t, unittest.PrepareTestDatabase()) 206 207 for _, test := range []struct { 207 - Opts issues_model.UserIssueStatsOptions 208 + FilterMode int 209 + Opts issues_model.IssuesOptions 208 210 ExpectedIssueStats issues_model.IssueStats 209 211 }{ 210 212 { 211 - issues_model.UserIssueStatsOptions{ 212 - UserID: 1, 213 - RepoIDs: []int64{1}, 214 - FilterMode: issues_model.FilterModeAll, 213 + issues_model.FilterModeAll, 214 + issues_model.IssuesOptions{ 215 + User: unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}), 216 + RepoIDs: []int64{1}, 217 + IsPull: util.OptionalBoolFalse, 215 218 }, 216 219 issues_model.IssueStats{ 217 220 YourRepositoriesCount: 1, // 6 ··· 222 225 }, 223 226 }, 224 227 { 225 - issues_model.UserIssueStatsOptions{ 226 - UserID: 1, 227 - RepoIDs: []int64{1}, 228 - FilterMode: issues_model.FilterModeAll, 229 - IsClosed: true, 228 + issues_model.FilterModeAll, 229 + issues_model.IssuesOptions{ 230 + User: unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}), 231 + RepoIDs: []int64{1}, 232 + IsPull: util.OptionalBoolFalse, 233 + IsClosed: util.OptionalBoolTrue, 230 234 }, 231 235 issues_model.IssueStats{ 232 236 YourRepositoriesCount: 1, // 6 ··· 237 241 }, 238 242 }, 239 243 { 240 - issues_model.UserIssueStatsOptions{ 241 - UserID: 1, 242 - FilterMode: issues_model.FilterModeAssign, 244 + issues_model.FilterModeAssign, 245 + issues_model.IssuesOptions{ 246 + User: unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}), 247 + IsPull: util.OptionalBoolFalse, 243 248 }, 244 249 issues_model.IssueStats{ 245 250 YourRepositoriesCount: 1, // 6 ··· 250 255 }, 251 256 }, 252 257 { 253 - issues_model.UserIssueStatsOptions{ 254 - UserID: 1, 255 - FilterMode: issues_model.FilterModeCreate, 258 + issues_model.FilterModeCreate, 259 + issues_model.IssuesOptions{ 260 + User: unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}), 261 + IsPull: util.OptionalBoolFalse, 256 262 }, 257 263 issues_model.IssueStats{ 258 264 YourRepositoriesCount: 1, // 6 ··· 263 269 }, 264 270 }, 265 271 { 266 - issues_model.UserIssueStatsOptions{ 267 - UserID: 1, 268 - FilterMode: issues_model.FilterModeMention, 272 + issues_model.FilterModeMention, 273 + issues_model.IssuesOptions{ 274 + User: unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}), 275 + IsPull: util.OptionalBoolFalse, 269 276 }, 270 277 issues_model.IssueStats{ 271 278 YourRepositoriesCount: 1, // 6 ··· 277 284 }, 278 285 }, 279 286 { 280 - issues_model.UserIssueStatsOptions{ 281 - UserID: 1, 282 - FilterMode: issues_model.FilterModeCreate, 283 - IssueIDs: []int64{1}, 287 + issues_model.FilterModeCreate, 288 + issues_model.IssuesOptions{ 289 + User: unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}), 290 + IssueIDs: []int64{1}, 291 + IsPull: util.OptionalBoolFalse, 284 292 }, 285 293 issues_model.IssueStats{ 286 294 YourRepositoriesCount: 1, // 1 ··· 291 299 }, 292 300 }, 293 301 { 294 - issues_model.UserIssueStatsOptions{ 295 - UserID: 2, 296 - Org: unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3}), 297 - Team: unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 7}), 298 - FilterMode: issues_model.FilterModeAll, 302 + issues_model.FilterModeAll, 303 + issues_model.IssuesOptions{ 304 + User: unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}), 305 + Org: unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3}), 306 + Team: unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 7}), 307 + IsPull: util.OptionalBoolFalse, 299 308 }, 300 309 issues_model.IssueStats{ 301 310 YourRepositoriesCount: 2, ··· 306 315 }, 307 316 } { 308 317 t.Run(fmt.Sprintf("%#v", test.Opts), func(t *testing.T) { 309 - stats, err := issues_model.GetUserIssueStats(test.Opts) 318 + stats, err := issues_model.GetUserIssueStats(test.FilterMode, test.Opts) 310 319 if !assert.NoError(t, err) { 311 320 return 312 321 } ··· 495 504 // Now we will call the GetIssueStats with these IDs and if working, 496 505 // get the correct stats back. 497 506 issueStats, err := issues_model.GetIssueStats(&issues_model.IssuesOptions{ 498 - RepoID: 1, 507 + RepoIDs: []int64{1}, 499 508 IssueIDs: ids, 500 509 }) 501 510
+1 -1
models/issues/issue_update.go
··· 81 81 } 82 82 83 83 // Update issue count of labels 84 - if err := issue.getLabels(ctx); err != nil { 84 + if err := issue.LoadLabels(ctx); err != nil { 85 85 return nil, err 86 86 } 87 87 for idx := range issue.Labels {
+1 -336
models/issues/label.go
··· 11 11 "strings" 12 12 13 13 "code.gitea.io/gitea/models/db" 14 - user_model "code.gitea.io/gitea/models/user" 15 14 "code.gitea.io/gitea/modules/label" 16 15 "code.gitea.io/gitea/modules/timeutil" 17 16 "code.gitea.io/gitea/modules/util" ··· 113 112 // CalOpenOrgIssues calculates the open issues of a label for a specific repo 114 113 func (l *Label) CalOpenOrgIssues(ctx context.Context, repoID, labelID int64) { 115 114 counts, _ := CountIssuesByRepo(ctx, &IssuesOptions{ 116 - RepoID: repoID, 115 + RepoIDs: []int64{repoID}, 117 116 LabelIDs: []int64{labelID}, 118 117 IsClosed: util.OptionalBoolFalse, 119 118 }) ··· 282 281 Find(&labels) 283 282 } 284 283 285 - // __________ .__ __ 286 - // \______ \ ____ ______ ____ _____|__|/ |_ ___________ ___.__. 287 - // | _// __ \\____ \ / _ \/ ___/ \ __\/ _ \_ __ < | | 288 - // | | \ ___/| |_> > <_> )___ \| || | ( <_> ) | \/\___ | 289 - // |____|_ /\___ > __/ \____/____ >__||__| \____/|__| / ____| 290 - // \/ \/|__| \/ \/ 291 - 292 284 // GetLabelInRepoByName returns a label by name in given repository. 293 285 func GetLabelInRepoByName(ctx context.Context, repoID int64, labelName string) (*Label, error) { 294 286 if len(labelName) == 0 || repoID <= 0 { ··· 393 385 return db.GetEngine(db.DefaultContext).Where("repo_id = ?", repoID).Count(&Label{}) 394 386 } 395 387 396 - // ________ 397 - // \_____ \_______ ____ 398 - // / | \_ __ \/ ___\ 399 - // / | \ | \/ /_/ > 400 - // \_______ /__| \___ / 401 - // \/ /_____/ 402 - 403 388 // GetLabelInOrgByName returns a label by name in given organization. 404 389 func GetLabelInOrgByName(ctx context.Context, orgID int64, labelName string) (*Label, error) { 405 390 if len(labelName) == 0 || orgID <= 0 { ··· 496 481 return db.GetEngine(db.DefaultContext).Where("org_id = ?", orgID).Count(&Label{}) 497 482 } 498 483 499 - // .___ 500 - // | | ______ ________ __ ____ 501 - // | |/ ___// ___/ | \_/ __ \ 502 - // | |\___ \ \___ \| | /\ ___/ 503 - // |___/____ >____ >____/ \___ | 504 - // \/ \/ \/ 505 - 506 - // GetLabelsByIssueID returns all labels that belong to given issue by ID. 507 - func GetLabelsByIssueID(ctx context.Context, issueID int64) ([]*Label, error) { 508 - var labels []*Label 509 - return labels, db.GetEngine(ctx).Where("issue_label.issue_id = ?", issueID). 510 - Join("LEFT", "issue_label", "issue_label.label_id = label.id"). 511 - Asc("label.name"). 512 - Find(&labels) 513 - } 514 - 515 484 func updateLabelCols(ctx context.Context, l *Label, cols ...string) error { 516 485 _, err := db.GetEngine(ctx).ID(l.ID). 517 486 SetExpr("num_issues", ··· 529 498 Cols(cols...).Update(l) 530 499 return err 531 500 } 532 - 533 - // .___ .____ ___. .__ 534 - // | | ______ ________ __ ____ | | _____ \_ |__ ____ | | 535 - // | |/ ___// ___/ | \_/ __ \| | \__ \ | __ \_/ __ \| | 536 - // | |\___ \ \___ \| | /\ ___/| |___ / __ \| \_\ \ ___/| |__ 537 - // |___/____ >____ >____/ \___ >_______ (____ /___ /\___ >____/ 538 - // \/ \/ \/ \/ \/ \/ \/ 539 - 540 - // IssueLabel represents an issue-label relation. 541 - type IssueLabel struct { 542 - ID int64 `xorm:"pk autoincr"` 543 - IssueID int64 `xorm:"UNIQUE(s)"` 544 - LabelID int64 `xorm:"UNIQUE(s)"` 545 - } 546 - 547 - // HasIssueLabel returns true if issue has been labeled. 548 - func HasIssueLabel(ctx context.Context, issueID, labelID int64) bool { 549 - has, _ := db.GetEngine(ctx).Where("issue_id = ? AND label_id = ?", issueID, labelID).Get(new(IssueLabel)) 550 - return has 551 - } 552 - 553 - // newIssueLabel this function creates a new label it does not check if the label is valid for the issue 554 - // YOU MUST CHECK THIS BEFORE THIS FUNCTION 555 - func newIssueLabel(ctx context.Context, issue *Issue, label *Label, doer *user_model.User) (err error) { 556 - if err = db.Insert(ctx, &IssueLabel{ 557 - IssueID: issue.ID, 558 - LabelID: label.ID, 559 - }); err != nil { 560 - return err 561 - } 562 - 563 - if err = issue.LoadRepo(ctx); err != nil { 564 - return 565 - } 566 - 567 - opts := &CreateCommentOptions{ 568 - Type: CommentTypeLabel, 569 - Doer: doer, 570 - Repo: issue.Repo, 571 - Issue: issue, 572 - Label: label, 573 - Content: "1", 574 - } 575 - if _, err = CreateComment(ctx, opts); err != nil { 576 - return err 577 - } 578 - 579 - return updateLabelCols(ctx, label, "num_issues", "num_closed_issue") 580 - } 581 - 582 - // Remove all issue labels in the given exclusive scope 583 - func RemoveDuplicateExclusiveIssueLabels(ctx context.Context, issue *Issue, label *Label, doer *user_model.User) (err error) { 584 - scope := label.ExclusiveScope() 585 - if scope == "" { 586 - return nil 587 - } 588 - 589 - var toRemove []*Label 590 - for _, issueLabel := range issue.Labels { 591 - if label.ID != issueLabel.ID && issueLabel.ExclusiveScope() == scope { 592 - toRemove = append(toRemove, issueLabel) 593 - } 594 - } 595 - 596 - for _, issueLabel := range toRemove { 597 - if err = deleteIssueLabel(ctx, issue, issueLabel, doer); err != nil { 598 - return err 599 - } 600 - } 601 - 602 - return nil 603 - } 604 - 605 - // NewIssueLabel creates a new issue-label relation. 606 - func NewIssueLabel(issue *Issue, label *Label, doer *user_model.User) (err error) { 607 - if HasIssueLabel(db.DefaultContext, issue.ID, label.ID) { 608 - return nil 609 - } 610 - 611 - ctx, committer, err := db.TxContext(db.DefaultContext) 612 - if err != nil { 613 - return err 614 - } 615 - defer committer.Close() 616 - 617 - if err = issue.LoadRepo(ctx); err != nil { 618 - return err 619 - } 620 - 621 - // Do NOT add invalid labels 622 - if issue.RepoID != label.RepoID && issue.Repo.OwnerID != label.OrgID { 623 - return nil 624 - } 625 - 626 - if err = RemoveDuplicateExclusiveIssueLabels(ctx, issue, label, doer); err != nil { 627 - return nil 628 - } 629 - 630 - if err = newIssueLabel(ctx, issue, label, doer); err != nil { 631 - return err 632 - } 633 - 634 - issue.Labels = nil 635 - if err = issue.LoadLabels(ctx); err != nil { 636 - return err 637 - } 638 - 639 - return committer.Commit() 640 - } 641 - 642 - // newIssueLabels add labels to an issue. It will check if the labels are valid for the issue 643 - func newIssueLabels(ctx context.Context, issue *Issue, labels []*Label, doer *user_model.User) (err error) { 644 - if err = issue.LoadRepo(ctx); err != nil { 645 - return err 646 - } 647 - for _, l := range labels { 648 - // Don't add already present labels and invalid labels 649 - if HasIssueLabel(ctx, issue.ID, l.ID) || 650 - (l.RepoID != issue.RepoID && l.OrgID != issue.Repo.OwnerID) { 651 - continue 652 - } 653 - 654 - if err = newIssueLabel(ctx, issue, l, doer); err != nil { 655 - return fmt.Errorf("newIssueLabel: %w", err) 656 - } 657 - } 658 - 659 - return nil 660 - } 661 - 662 - // NewIssueLabels creates a list of issue-label relations. 663 - func NewIssueLabels(issue *Issue, labels []*Label, doer *user_model.User) (err error) { 664 - ctx, committer, err := db.TxContext(db.DefaultContext) 665 - if err != nil { 666 - return err 667 - } 668 - defer committer.Close() 669 - 670 - if err = newIssueLabels(ctx, issue, labels, doer); err != nil { 671 - return err 672 - } 673 - 674 - issue.Labels = nil 675 - if err = issue.LoadLabels(ctx); err != nil { 676 - return err 677 - } 678 - 679 - return committer.Commit() 680 - } 681 - 682 - func deleteIssueLabel(ctx context.Context, issue *Issue, label *Label, doer *user_model.User) (err error) { 683 - if count, err := db.DeleteByBean(ctx, &IssueLabel{ 684 - IssueID: issue.ID, 685 - LabelID: label.ID, 686 - }); err != nil { 687 - return err 688 - } else if count == 0 { 689 - return nil 690 - } 691 - 692 - if err = issue.LoadRepo(ctx); err != nil { 693 - return 694 - } 695 - 696 - opts := &CreateCommentOptions{ 697 - Type: CommentTypeLabel, 698 - Doer: doer, 699 - Repo: issue.Repo, 700 - Issue: issue, 701 - Label: label, 702 - } 703 - if _, err = CreateComment(ctx, opts); err != nil { 704 - return err 705 - } 706 - 707 - return updateLabelCols(ctx, label, "num_issues", "num_closed_issue") 708 - } 709 - 710 - // DeleteIssueLabel deletes issue-label relation. 711 - func DeleteIssueLabel(ctx context.Context, issue *Issue, label *Label, doer *user_model.User) error { 712 - if err := deleteIssueLabel(ctx, issue, label, doer); err != nil { 713 - return err 714 - } 715 - 716 - issue.Labels = nil 717 - return issue.LoadLabels(ctx) 718 - } 719 - 720 - // DeleteLabelsByRepoID deletes labels of some repository 721 - func DeleteLabelsByRepoID(ctx context.Context, repoID int64) error { 722 - deleteCond := builder.Select("id").From("label").Where(builder.Eq{"label.repo_id": repoID}) 723 - 724 - if _, err := db.GetEngine(ctx).In("label_id", deleteCond). 725 - Delete(&IssueLabel{}); err != nil { 726 - return err 727 - } 728 - 729 - _, err := db.DeleteByBean(ctx, &Label{RepoID: repoID}) 730 - return err 731 - } 732 - 733 - // CountOrphanedLabels return count of labels witch are broken and not accessible via ui anymore 734 - func CountOrphanedLabels(ctx context.Context) (int64, error) { 735 - noref, err := db.GetEngine(ctx).Table("label").Where("repo_id=? AND org_id=?", 0, 0).Count() 736 - if err != nil { 737 - return 0, err 738 - } 739 - 740 - norepo, err := db.GetEngine(ctx).Table("label"). 741 - Where(builder.And( 742 - builder.Gt{"repo_id": 0}, 743 - builder.NotIn("repo_id", builder.Select("id").From("`repository`")), 744 - )). 745 - Count() 746 - if err != nil { 747 - return 0, err 748 - } 749 - 750 - noorg, err := db.GetEngine(ctx).Table("label"). 751 - Where(builder.And( 752 - builder.Gt{"org_id": 0}, 753 - builder.NotIn("org_id", builder.Select("id").From("`user`")), 754 - )). 755 - Count() 756 - if err != nil { 757 - return 0, err 758 - } 759 - 760 - return noref + norepo + noorg, nil 761 - } 762 - 763 - // DeleteOrphanedLabels delete labels witch are broken and not accessible via ui anymore 764 - func DeleteOrphanedLabels(ctx context.Context) error { 765 - // delete labels with no reference 766 - if _, err := db.GetEngine(ctx).Table("label").Where("repo_id=? AND org_id=?", 0, 0).Delete(new(Label)); err != nil { 767 - return err 768 - } 769 - 770 - // delete labels with none existing repos 771 - if _, err := db.GetEngine(ctx). 772 - Where(builder.And( 773 - builder.Gt{"repo_id": 0}, 774 - builder.NotIn("repo_id", builder.Select("id").From("`repository`")), 775 - )). 776 - Delete(Label{}); err != nil { 777 - return err 778 - } 779 - 780 - // delete labels with none existing orgs 781 - if _, err := db.GetEngine(ctx). 782 - Where(builder.And( 783 - builder.Gt{"org_id": 0}, 784 - builder.NotIn("org_id", builder.Select("id").From("`user`")), 785 - )). 786 - Delete(Label{}); err != nil { 787 - return err 788 - } 789 - 790 - return nil 791 - } 792 - 793 - // CountOrphanedIssueLabels return count of IssueLabels witch have no label behind anymore 794 - func CountOrphanedIssueLabels(ctx context.Context) (int64, error) { 795 - return db.GetEngine(ctx).Table("issue_label"). 796 - NotIn("label_id", builder.Select("id").From("label")). 797 - Count() 798 - } 799 - 800 - // DeleteOrphanedIssueLabels delete IssueLabels witch have no label behind anymore 801 - func DeleteOrphanedIssueLabels(ctx context.Context) error { 802 - _, err := db.GetEngine(ctx). 803 - NotIn("label_id", builder.Select("id").From("label")). 804 - Delete(IssueLabel{}) 805 - return err 806 - } 807 - 808 - // CountIssueLabelWithOutsideLabels count label comments with outside label 809 - func CountIssueLabelWithOutsideLabels(ctx context.Context) (int64, error) { 810 - 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)")). 811 - Table("issue_label"). 812 - Join("inner", "label", "issue_label.label_id = label.id "). 813 - Join("inner", "issue", "issue.id = issue_label.issue_id "). 814 - Join("inner", "repository", "issue.repo_id = repository.id"). 815 - Count(new(IssueLabel)) 816 - } 817 - 818 - // FixIssueLabelWithOutsideLabels fix label comments with outside label 819 - func FixIssueLabelWithOutsideLabels(ctx context.Context) (int64, error) { 820 - res, err := db.GetEngine(ctx).Exec(`DELETE FROM issue_label WHERE issue_label.id IN ( 821 - SELECT il_too.id FROM ( 822 - SELECT il_too_too.id 823 - FROM issue_label AS il_too_too 824 - INNER JOIN label ON il_too_too.label_id = label.id 825 - INNER JOIN issue on issue.id = il_too_too.issue_id 826 - INNER JOIN repository on repository.id = issue.repo_id 827 - WHERE 828 - (label.org_id = 0 AND issue.repo_id != label.repo_id) OR (label.repo_id = 0 AND label.org_id != repository.owner_id) 829 - ) AS il_too )`) 830 - if err != nil { 831 - return 0, err 832 - } 833 - 834 - return res.RowsAffected() 835 - }
+1 -1
modules/indexer/issues/indexer.go
··· 302 302 // UpdateRepoIndexer add/update all issues of the repositories 303 303 func UpdateRepoIndexer(ctx context.Context, repo *repo_model.Repository) { 304 304 is, err := issues_model.Issues(ctx, &issues_model.IssuesOptions{ 305 - RepoID: repo.ID, 305 + RepoIDs: []int64{repo.ID}, 306 306 IsClosed: util.OptionalBoolNone, 307 307 IsPull: util.OptionalBoolNone, 308 308 })
+1 -1
routers/api/v1/repo/issue.go
··· 470 470 if len(keyword) == 0 || len(issueIDs) > 0 || len(labelIDs) > 0 { 471 471 issuesOpt := &issues_model.IssuesOptions{ 472 472 ListOptions: listOptions, 473 - RepoID: ctx.Repo.Repository.ID, 473 + RepoIDs: []int64{ctx.Repo.Repository.ID}, 474 474 IsClosed: isClosed, 475 475 IssueIDs: issueIDs, 476 476 LabelIDs: labelIDs,
+3 -3
routers/web/repo/issue.go
··· 207 207 issueStats = &issues_model.IssueStats{} 208 208 } else { 209 209 issueStats, err = issues_model.GetIssueStats(&issues_model.IssuesOptions{ 210 - RepoID: repo.ID, 210 + RepoIDs: []int64{repo.ID}, 211 211 LabelIDs: labelIDs, 212 212 MilestoneIDs: []int64{milestoneID}, 213 213 ProjectID: projectID, ··· 258 258 Page: pager.Paginater.Current(), 259 259 PageSize: setting.UI.IssuePagingNum, 260 260 }, 261 - RepoID: repo.ID, 261 + RepoIDs: []int64{repo.ID}, 262 262 AssigneeID: assigneeID, 263 263 PosterID: posterID, 264 264 MentionedID: mentionedID, ··· 2652 2652 if len(keyword) == 0 || len(issueIDs) > 0 || len(labelIDs) > 0 { 2653 2653 issuesOpt := &issues_model.IssuesOptions{ 2654 2654 ListOptions: listOptions, 2655 - RepoID: ctx.Repo.Repository.ID, 2655 + RepoIDs: []int64{ctx.Repo.Repository.ID}, 2656 2656 IsClosed: isClosed, 2657 2657 IssueIDs: issueIDs, 2658 2658 LabelIDs: labelIDs,
+12 -16
routers/web/user/home.go
··· 521 521 522 522 // Parse ctx.FormString("repos") and remember matched repo IDs for later. 523 523 // Gets set when clicking filters on the issues overview page. 524 - repoIDs := getRepoIDs(ctx.FormString("repos")) 525 - if len(repoIDs) > 0 { 526 - opts.RepoCond = builder.In("issue.repo_id", repoIDs) 527 - } 524 + opts.RepoIDs = getRepoIDs(ctx.FormString("repos")) 528 525 529 526 // ------------------------------ 530 527 // Get issues as defined by opts. ··· 580 577 // ------------------------------- 581 578 var issueStats *issues_model.IssueStats 582 579 if !forceEmpty { 583 - statsOpts := issues_model.UserIssueStatsOptions{ 584 - UserID: ctx.Doer.ID, 585 - FilterMode: filterMode, 586 - IsPull: isPullList, 587 - IsClosed: isShowClosed, 580 + statsOpts := issues_model.IssuesOptions{ 581 + User: ctx.Doer, 582 + IsPull: util.OptionalBoolOf(isPullList), 583 + IsClosed: util.OptionalBoolOf(isShowClosed), 588 584 IssueIDs: issueIDsFromSearch, 589 585 IsArchived: util.OptionalBoolFalse, 590 586 LabelIDs: opts.LabelIDs, ··· 593 589 RepoCond: opts.RepoCond, 594 590 } 595 591 596 - issueStats, err = issues_model.GetUserIssueStats(statsOpts) 592 + issueStats, err = issues_model.GetUserIssueStats(filterMode, statsOpts) 597 593 if err != nil { 598 594 ctx.ServerError("GetUserIssueStats Shown", err) 599 595 return ··· 609 605 } else { 610 606 shownIssues = int(issueStats.ClosedCount) 611 607 } 612 - if len(repoIDs) != 0 { 608 + if len(opts.RepoIDs) != 0 { 613 609 shownIssues = 0 614 - for _, repoID := range repoIDs { 610 + for _, repoID := range opts.RepoIDs { 615 611 shownIssues += int(issueCountByRepo[repoID]) 616 612 } 617 613 } ··· 622 618 } 623 619 ctx.Data["TotalIssueCount"] = allIssueCount 624 620 625 - if len(repoIDs) == 1 { 626 - repo := showReposMap[repoIDs[0]] 621 + if len(opts.RepoIDs) == 1 { 622 + repo := showReposMap[opts.RepoIDs[0]] 627 623 if repo != nil { 628 624 ctx.Data["SingleRepoLink"] = repo.Link() 629 625 } ··· 665 661 ctx.Data["IssueStats"] = issueStats 666 662 ctx.Data["ViewType"] = viewType 667 663 ctx.Data["SortType"] = sortType 668 - ctx.Data["RepoIDs"] = repoIDs 664 + ctx.Data["RepoIDs"] = opts.RepoIDs 669 665 ctx.Data["IsShowClosed"] = isShowClosed 670 666 ctx.Data["SelectLabels"] = selectedLabels 671 667 ··· 676 672 } 677 673 678 674 // Convert []int64 to string 679 - reposParam, _ := json.Marshal(repoIDs) 675 + reposParam, _ := json.Marshal(opts.RepoIDs) 680 676 681 677 ctx.Data["ReposParam"] = string(reposParam) 682 678
+1 -1
services/migrations/gitea_uploader_test.go
··· 104 104 assert.Len(t, releases, 1) 105 105 106 106 issues, err := issues_model.Issues(db.DefaultContext, &issues_model.IssuesOptions{ 107 - RepoID: repo.ID, 107 + RepoIDs: []int64{repo.ID}, 108 108 IsPull: util.OptionalBoolFalse, 109 109 SortType: "oldest", 110 110 })