code.gitea.io/gitea@v1.22.3/models/issues/issue_update.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 "strings" 10 11 "code.gitea.io/gitea/models/db" 12 "code.gitea.io/gitea/models/organization" 13 "code.gitea.io/gitea/models/perm" 14 access_model "code.gitea.io/gitea/models/perm/access" 15 project_model "code.gitea.io/gitea/models/project" 16 repo_model "code.gitea.io/gitea/models/repo" 17 system_model "code.gitea.io/gitea/models/system" 18 "code.gitea.io/gitea/models/unit" 19 user_model "code.gitea.io/gitea/models/user" 20 "code.gitea.io/gitea/modules/git" 21 "code.gitea.io/gitea/modules/references" 22 api "code.gitea.io/gitea/modules/structs" 23 "code.gitea.io/gitea/modules/timeutil" 24 25 "xorm.io/builder" 26 ) 27 28 // UpdateIssueCols updates cols of issue 29 func UpdateIssueCols(ctx context.Context, issue *Issue, cols ...string) error { 30 if _, err := db.GetEngine(ctx).ID(issue.ID).Cols(cols...).Update(issue); err != nil { 31 return err 32 } 33 return nil 34 } 35 36 func changeIssueStatus(ctx context.Context, issue *Issue, doer *user_model.User, isClosed, isMergePull bool) (*Comment, error) { 37 // Reload the issue 38 currentIssue, err := GetIssueByID(ctx, issue.ID) 39 if err != nil { 40 return nil, err 41 } 42 43 // Nothing should be performed if current status is same as target status 44 if currentIssue.IsClosed == isClosed { 45 if !issue.IsPull { 46 return nil, ErrIssueWasClosed{ 47 ID: issue.ID, 48 } 49 } 50 return nil, ErrPullWasClosed{ 51 ID: issue.ID, 52 } 53 } 54 55 issue.IsClosed = isClosed 56 return doChangeIssueStatus(ctx, issue, doer, isMergePull) 57 } 58 59 func doChangeIssueStatus(ctx context.Context, issue *Issue, doer *user_model.User, isMergePull bool) (*Comment, error) { 60 // Check for open dependencies 61 if issue.IsClosed && issue.Repo.IsDependenciesEnabled(ctx) { 62 // only check if dependencies are enabled and we're about to close an issue, otherwise reopening an issue would fail when there are unsatisfied dependencies 63 noDeps, err := IssueNoDependenciesLeft(ctx, issue) 64 if err != nil { 65 return nil, err 66 } 67 68 if !noDeps { 69 return nil, ErrDependenciesLeft{issue.ID} 70 } 71 } 72 73 if issue.IsClosed { 74 issue.ClosedUnix = timeutil.TimeStampNow() 75 } else { 76 issue.ClosedUnix = 0 77 } 78 79 if err := UpdateIssueCols(ctx, issue, "is_closed", "closed_unix"); err != nil { 80 return nil, err 81 } 82 83 // Update issue count of labels 84 if err := issue.LoadLabels(ctx); err != nil { 85 return nil, err 86 } 87 for idx := range issue.Labels { 88 if err := updateLabelCols(ctx, issue.Labels[idx], "num_issues", "num_closed_issue"); err != nil { 89 return nil, err 90 } 91 } 92 93 // Update issue count of milestone 94 if issue.MilestoneID > 0 { 95 if err := UpdateMilestoneCounters(ctx, issue.MilestoneID); err != nil { 96 return nil, err 97 } 98 } 99 100 // update repository's issue closed number 101 if err := repo_model.UpdateRepoIssueNumbers(ctx, issue.RepoID, issue.IsPull, true); err != nil { 102 return nil, err 103 } 104 105 // New action comment 106 cmtType := CommentTypeClose 107 if !issue.IsClosed { 108 cmtType = CommentTypeReopen 109 } else if isMergePull { 110 cmtType = CommentTypeMergePull 111 } 112 113 return CreateComment(ctx, &CreateCommentOptions{ 114 Type: cmtType, 115 Doer: doer, 116 Repo: issue.Repo, 117 Issue: issue, 118 }) 119 } 120 121 // ChangeIssueStatus changes issue status to open or closed. 122 func ChangeIssueStatus(ctx context.Context, issue *Issue, doer *user_model.User, isClosed bool) (*Comment, error) { 123 if err := issue.LoadRepo(ctx); err != nil { 124 return nil, err 125 } 126 if err := issue.LoadPoster(ctx); err != nil { 127 return nil, err 128 } 129 130 return changeIssueStatus(ctx, issue, doer, isClosed, false) 131 } 132 133 // ChangeIssueTitle changes the title of this issue, as the given user. 134 func ChangeIssueTitle(ctx context.Context, issue *Issue, doer *user_model.User, oldTitle string) (err error) { 135 ctx, committer, err := db.TxContext(ctx) 136 if err != nil { 137 return err 138 } 139 defer committer.Close() 140 141 if err = UpdateIssueCols(ctx, issue, "name"); err != nil { 142 return fmt.Errorf("updateIssueCols: %w", err) 143 } 144 145 if err = issue.LoadRepo(ctx); err != nil { 146 return fmt.Errorf("loadRepo: %w", err) 147 } 148 149 opts := &CreateCommentOptions{ 150 Type: CommentTypeChangeTitle, 151 Doer: doer, 152 Repo: issue.Repo, 153 Issue: issue, 154 OldTitle: oldTitle, 155 NewTitle: issue.Title, 156 } 157 if _, err = CreateComment(ctx, opts); err != nil { 158 return fmt.Errorf("createComment: %w", err) 159 } 160 if err = issue.AddCrossReferences(ctx, doer, true); err != nil { 161 return err 162 } 163 164 return committer.Commit() 165 } 166 167 // ChangeIssueRef changes the branch of this issue, as the given user. 168 func ChangeIssueRef(ctx context.Context, issue *Issue, doer *user_model.User, oldRef string) (err error) { 169 ctx, committer, err := db.TxContext(ctx) 170 if err != nil { 171 return err 172 } 173 defer committer.Close() 174 175 if err = UpdateIssueCols(ctx, issue, "ref"); err != nil { 176 return fmt.Errorf("updateIssueCols: %w", err) 177 } 178 179 if err = issue.LoadRepo(ctx); err != nil { 180 return fmt.Errorf("loadRepo: %w", err) 181 } 182 oldRefFriendly := strings.TrimPrefix(oldRef, git.BranchPrefix) 183 newRefFriendly := strings.TrimPrefix(issue.Ref, git.BranchPrefix) 184 185 opts := &CreateCommentOptions{ 186 Type: CommentTypeChangeIssueRef, 187 Doer: doer, 188 Repo: issue.Repo, 189 Issue: issue, 190 OldRef: oldRefFriendly, 191 NewRef: newRefFriendly, 192 } 193 if _, err = CreateComment(ctx, opts); err != nil { 194 return fmt.Errorf("createComment: %w", err) 195 } 196 197 return committer.Commit() 198 } 199 200 // AddDeletePRBranchComment adds delete branch comment for pull request issue 201 func AddDeletePRBranchComment(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, issueID int64, branchName string) error { 202 issue, err := GetIssueByID(ctx, issueID) 203 if err != nil { 204 return err 205 } 206 opts := &CreateCommentOptions{ 207 Type: CommentTypeDeleteBranch, 208 Doer: doer, 209 Repo: repo, 210 Issue: issue, 211 OldRef: branchName, 212 } 213 _, err = CreateComment(ctx, opts) 214 return err 215 } 216 217 // UpdateIssueAttachments update attachments by UUIDs for the issue 218 func UpdateIssueAttachments(ctx context.Context, issueID int64, uuids []string) (err error) { 219 ctx, committer, err := db.TxContext(ctx) 220 if err != nil { 221 return err 222 } 223 defer committer.Close() 224 attachments, err := repo_model.GetAttachmentsByUUIDs(ctx, uuids) 225 if err != nil { 226 return fmt.Errorf("getAttachmentsByUUIDs [uuids: %v]: %w", uuids, err) 227 } 228 for i := 0; i < len(attachments); i++ { 229 attachments[i].IssueID = issueID 230 if err := repo_model.UpdateAttachment(ctx, attachments[i]); err != nil { 231 return fmt.Errorf("update attachment [id: %d]: %w", attachments[i].ID, err) 232 } 233 } 234 return committer.Commit() 235 } 236 237 // ChangeIssueContent changes issue content, as the given user. 238 func ChangeIssueContent(ctx context.Context, issue *Issue, doer *user_model.User, content string) (err error) { 239 ctx, committer, err := db.TxContext(ctx) 240 if err != nil { 241 return err 242 } 243 defer committer.Close() 244 245 hasContentHistory, err := HasIssueContentHistory(ctx, issue.ID, 0) 246 if err != nil { 247 return fmt.Errorf("HasIssueContentHistory: %w", err) 248 } 249 if !hasContentHistory { 250 if err = SaveIssueContentHistory(ctx, issue.PosterID, issue.ID, 0, 251 issue.CreatedUnix, issue.Content, true); err != nil { 252 return fmt.Errorf("SaveIssueContentHistory: %w", err) 253 } 254 } 255 256 issue.Content = content 257 258 if err = UpdateIssueCols(ctx, issue, "content"); err != nil { 259 return fmt.Errorf("UpdateIssueCols: %w", err) 260 } 261 262 if err = SaveIssueContentHistory(ctx, doer.ID, issue.ID, 0, 263 timeutil.TimeStampNow(), issue.Content, false); err != nil { 264 return fmt.Errorf("SaveIssueContentHistory: %w", err) 265 } 266 267 if err = issue.AddCrossReferences(ctx, doer, true); err != nil { 268 return fmt.Errorf("addCrossReferences: %w", err) 269 } 270 271 return committer.Commit() 272 } 273 274 // NewIssueOptions represents the options of a new issue. 275 type NewIssueOptions struct { 276 Repo *repo_model.Repository 277 Issue *Issue 278 LabelIDs []int64 279 Attachments []string // In UUID format. 280 IsPull bool 281 } 282 283 // NewIssueWithIndex creates issue with given index 284 func NewIssueWithIndex(ctx context.Context, doer *user_model.User, opts NewIssueOptions) (err error) { 285 e := db.GetEngine(ctx) 286 opts.Issue.Title = strings.TrimSpace(opts.Issue.Title) 287 288 if opts.Issue.MilestoneID > 0 { 289 milestone, err := GetMilestoneByRepoID(ctx, opts.Issue.RepoID, opts.Issue.MilestoneID) 290 if err != nil && !IsErrMilestoneNotExist(err) { 291 return fmt.Errorf("getMilestoneByID: %w", err) 292 } 293 294 // Assume milestone is invalid and drop silently. 295 opts.Issue.MilestoneID = 0 296 if milestone != nil { 297 opts.Issue.MilestoneID = milestone.ID 298 opts.Issue.Milestone = milestone 299 } 300 } 301 302 if opts.Issue.Index <= 0 { 303 return fmt.Errorf("no issue index provided") 304 } 305 if opts.Issue.ID > 0 { 306 return fmt.Errorf("issue exist") 307 } 308 309 if _, err := e.Insert(opts.Issue); err != nil { 310 return err 311 } 312 313 if opts.Issue.MilestoneID > 0 { 314 if err := UpdateMilestoneCounters(ctx, opts.Issue.MilestoneID); err != nil { 315 return err 316 } 317 318 opts := &CreateCommentOptions{ 319 Type: CommentTypeMilestone, 320 Doer: doer, 321 Repo: opts.Repo, 322 Issue: opts.Issue, 323 OldMilestoneID: 0, 324 MilestoneID: opts.Issue.MilestoneID, 325 } 326 if _, err = CreateComment(ctx, opts); err != nil { 327 return err 328 } 329 } 330 331 if err := repo_model.UpdateRepoIssueNumbers(ctx, opts.Issue.RepoID, opts.IsPull, false); err != nil { 332 return err 333 } 334 335 if len(opts.LabelIDs) > 0 { 336 // During the session, SQLite3 driver cannot handle retrieve objects after update something. 337 // So we have to get all needed labels first. 338 labels := make([]*Label, 0, len(opts.LabelIDs)) 339 if err = e.In("id", opts.LabelIDs).Find(&labels); err != nil { 340 return fmt.Errorf("find all labels [label_ids: %v]: %w", opts.LabelIDs, err) 341 } 342 343 if err = opts.Issue.LoadPoster(ctx); err != nil { 344 return err 345 } 346 347 for _, label := range labels { 348 // Silently drop invalid labels. 349 if label.RepoID != opts.Repo.ID && label.OrgID != opts.Repo.OwnerID { 350 continue 351 } 352 353 if err = newIssueLabel(ctx, opts.Issue, label, opts.Issue.Poster); err != nil { 354 return fmt.Errorf("addLabel [id: %d]: %w", label.ID, err) 355 } 356 } 357 } 358 359 if err = NewIssueUsers(ctx, opts.Repo, opts.Issue); err != nil { 360 return err 361 } 362 363 if len(opts.Attachments) > 0 { 364 attachments, err := repo_model.GetAttachmentsByUUIDs(ctx, opts.Attachments) 365 if err != nil { 366 return fmt.Errorf("getAttachmentsByUUIDs [uuids: %v]: %w", opts.Attachments, err) 367 } 368 369 for i := 0; i < len(attachments); i++ { 370 attachments[i].IssueID = opts.Issue.ID 371 if _, err = e.ID(attachments[i].ID).Update(attachments[i]); err != nil { 372 return fmt.Errorf("update attachment [id: %d]: %w", attachments[i].ID, err) 373 } 374 } 375 } 376 if err = opts.Issue.LoadAttributes(ctx); err != nil { 377 return err 378 } 379 380 return opts.Issue.AddCrossReferences(ctx, doer, false) 381 } 382 383 // NewIssue creates new issue with labels for repository. 384 func NewIssue(ctx context.Context, repo *repo_model.Repository, issue *Issue, labelIDs []int64, uuids []string) (err error) { 385 ctx, committer, err := db.TxContext(ctx) 386 if err != nil { 387 return err 388 } 389 defer committer.Close() 390 391 idx, err := db.GetNextResourceIndex(ctx, "issue_index", repo.ID) 392 if err != nil { 393 return fmt.Errorf("generate issue index failed: %w", err) 394 } 395 396 issue.Index = idx 397 398 if err = NewIssueWithIndex(ctx, issue.Poster, NewIssueOptions{ 399 Repo: repo, 400 Issue: issue, 401 LabelIDs: labelIDs, 402 Attachments: uuids, 403 }); err != nil { 404 if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) || IsErrNewIssueInsert(err) { 405 return err 406 } 407 return fmt.Errorf("newIssue: %w", err) 408 } 409 410 if err = committer.Commit(); err != nil { 411 return fmt.Errorf("Commit: %w", err) 412 } 413 414 return nil 415 } 416 417 // UpdateIssueMentions updates issue-user relations for mentioned users. 418 func UpdateIssueMentions(ctx context.Context, issueID int64, mentions []*user_model.User) error { 419 if len(mentions) == 0 { 420 return nil 421 } 422 ids := make([]int64, len(mentions)) 423 for i, u := range mentions { 424 ids[i] = u.ID 425 } 426 if err := UpdateIssueUsersByMentions(ctx, issueID, ids); err != nil { 427 return fmt.Errorf("UpdateIssueUsersByMentions: %w", err) 428 } 429 return nil 430 } 431 432 // UpdateIssueDeadline updates an issue deadline and adds comments. Setting a deadline to 0 means deleting it. 433 func UpdateIssueDeadline(ctx context.Context, issue *Issue, deadlineUnix timeutil.TimeStamp, doer *user_model.User) (err error) { 434 // if the deadline hasn't changed do nothing 435 if issue.DeadlineUnix == deadlineUnix { 436 return nil 437 } 438 ctx, committer, err := db.TxContext(ctx) 439 if err != nil { 440 return err 441 } 442 defer committer.Close() 443 444 // Update the deadline 445 if err = UpdateIssueCols(ctx, &Issue{ID: issue.ID, DeadlineUnix: deadlineUnix}, "deadline_unix"); err != nil { 446 return err 447 } 448 449 // Make the comment 450 if _, err = createDeadlineComment(ctx, doer, issue, deadlineUnix); err != nil { 451 return fmt.Errorf("createRemovedDueDateComment: %w", err) 452 } 453 454 return committer.Commit() 455 } 456 457 // FindAndUpdateIssueMentions finds users mentioned in the given content string, and saves them in the database. 458 func FindAndUpdateIssueMentions(ctx context.Context, issue *Issue, doer *user_model.User, content string) (mentions []*user_model.User, err error) { 459 rawMentions := references.FindAllMentionsMarkdown(content) 460 mentions, err = ResolveIssueMentionsByVisibility(ctx, issue, doer, rawMentions) 461 if err != nil { 462 return nil, fmt.Errorf("UpdateIssueMentions [%d]: %w", issue.ID, err) 463 } 464 465 notBlocked := make([]*user_model.User, 0, len(mentions)) 466 for _, user := range mentions { 467 if !user_model.IsUserBlockedBy(ctx, doer, user.ID) { 468 notBlocked = append(notBlocked, user) 469 } 470 } 471 mentions = notBlocked 472 473 if err = UpdateIssueMentions(ctx, issue.ID, mentions); err != nil { 474 return nil, fmt.Errorf("UpdateIssueMentions [%d]: %w", issue.ID, err) 475 } 476 return mentions, err 477 } 478 479 // ResolveIssueMentionsByVisibility returns the users mentioned in an issue, removing those that 480 // don't have access to reading it. Teams are expanded into their users, but organizations are ignored. 481 func ResolveIssueMentionsByVisibility(ctx context.Context, issue *Issue, doer *user_model.User, mentions []string) (users []*user_model.User, err error) { 482 if len(mentions) == 0 { 483 return nil, nil 484 } 485 if err = issue.LoadRepo(ctx); err != nil { 486 return nil, err 487 } 488 489 resolved := make(map[string]bool, 10) 490 var mentionTeams []string 491 492 if err := issue.Repo.LoadOwner(ctx); err != nil { 493 return nil, err 494 } 495 496 repoOwnerIsOrg := issue.Repo.Owner.IsOrganization() 497 if repoOwnerIsOrg { 498 mentionTeams = make([]string, 0, 5) 499 } 500 501 resolved[doer.LowerName] = true 502 for _, name := range mentions { 503 name := strings.ToLower(name) 504 if _, ok := resolved[name]; ok { 505 continue 506 } 507 if repoOwnerIsOrg && strings.Contains(name, "/") { 508 names := strings.Split(name, "/") 509 if len(names) < 2 || names[0] != issue.Repo.Owner.LowerName { 510 continue 511 } 512 mentionTeams = append(mentionTeams, names[1]) 513 resolved[name] = true 514 } else { 515 resolved[name] = false 516 } 517 } 518 519 if issue.Repo.Owner.IsOrganization() && len(mentionTeams) > 0 { 520 teams := make([]*organization.Team, 0, len(mentionTeams)) 521 if err := db.GetEngine(ctx). 522 Join("INNER", "team_repo", "team_repo.team_id = team.id"). 523 Where("team_repo.repo_id=?", issue.Repo.ID). 524 In("team.lower_name", mentionTeams). 525 Find(&teams); err != nil { 526 return nil, fmt.Errorf("find mentioned teams: %w", err) 527 } 528 if len(teams) != 0 { 529 checked := make([]int64, 0, len(teams)) 530 unittype := unit.TypeIssues 531 if issue.IsPull { 532 unittype = unit.TypePullRequests 533 } 534 for _, team := range teams { 535 if team.AccessMode >= perm.AccessModeAdmin { 536 checked = append(checked, team.ID) 537 resolved[issue.Repo.Owner.LowerName+"/"+team.LowerName] = true 538 continue 539 } 540 has, err := db.GetEngine(ctx).Get(&organization.TeamUnit{OrgID: issue.Repo.Owner.ID, TeamID: team.ID, Type: unittype}) 541 if err != nil { 542 return nil, fmt.Errorf("get team units (%d): %w", team.ID, err) 543 } 544 if has { 545 checked = append(checked, team.ID) 546 resolved[issue.Repo.Owner.LowerName+"/"+team.LowerName] = true 547 } 548 } 549 if len(checked) != 0 { 550 teamusers := make([]*user_model.User, 0, 20) 551 if err := db.GetEngine(ctx). 552 Join("INNER", "team_user", "team_user.uid = `user`.id"). 553 In("`team_user`.team_id", checked). 554 And("`user`.is_active = ?", true). 555 And("`user`.prohibit_login = ?", false). 556 Find(&teamusers); err != nil { 557 return nil, fmt.Errorf("get teams users: %w", err) 558 } 559 if len(teamusers) > 0 { 560 users = make([]*user_model.User, 0, len(teamusers)) 561 for _, user := range teamusers { 562 if already, ok := resolved[user.LowerName]; !ok || !already { 563 users = append(users, user) 564 resolved[user.LowerName] = true 565 } 566 } 567 } 568 } 569 } 570 } 571 572 // Remove names already in the list to avoid querying the database if pending names remain 573 mentionUsers := make([]string, 0, len(resolved)) 574 for name, already := range resolved { 575 if !already { 576 mentionUsers = append(mentionUsers, name) 577 } 578 } 579 if len(mentionUsers) == 0 { 580 return users, err 581 } 582 583 if users == nil { 584 users = make([]*user_model.User, 0, len(mentionUsers)) 585 } 586 587 unchecked := make([]*user_model.User, 0, len(mentionUsers)) 588 if err := db.GetEngine(ctx). 589 Where("`user`.is_active = ?", true). 590 And("`user`.prohibit_login = ?", false). 591 In("`user`.lower_name", mentionUsers). 592 Find(&unchecked); err != nil { 593 return nil, fmt.Errorf("find mentioned users: %w", err) 594 } 595 for _, user := range unchecked { 596 if already := resolved[user.LowerName]; already || user.IsOrganization() { 597 continue 598 } 599 // Normal users must have read access to the referencing issue 600 perm, err := access_model.GetUserRepoPermission(ctx, issue.Repo, user) 601 if err != nil { 602 return nil, fmt.Errorf("GetUserRepoPermission [%d]: %w", user.ID, err) 603 } 604 if !perm.CanReadIssuesOrPulls(issue.IsPull) { 605 continue 606 } 607 users = append(users, user) 608 } 609 610 return users, err 611 } 612 613 // UpdateIssuesMigrationsByType updates all migrated repositories' issues from gitServiceType to replace originalAuthorID to posterID 614 func UpdateIssuesMigrationsByType(ctx context.Context, gitServiceType api.GitServiceType, originalAuthorID string, posterID int64) error { 615 _, err := db.GetEngine(ctx).Table("issue"). 616 Where("repo_id IN (SELECT id FROM repository WHERE original_service_type = ?)", gitServiceType). 617 And("original_author_id = ?", originalAuthorID). 618 Update(map[string]any{ 619 "poster_id": posterID, 620 "original_author": "", 621 "original_author_id": 0, 622 }) 623 return err 624 } 625 626 // UpdateReactionsMigrationsByType updates all migrated repositories' reactions from gitServiceType to replace originalAuthorID to posterID 627 func UpdateReactionsMigrationsByType(ctx context.Context, gitServiceType api.GitServiceType, originalAuthorID string, userID int64) error { 628 _, err := db.GetEngine(ctx).Table("reaction"). 629 Where("original_author_id = ?", originalAuthorID). 630 And(migratedIssueCond(gitServiceType)). 631 Update(map[string]any{ 632 "user_id": userID, 633 "original_author": "", 634 "original_author_id": 0, 635 }) 636 return err 637 } 638 639 // DeleteIssuesByRepoID deletes issues by repositories id 640 func DeleteIssuesByRepoID(ctx context.Context, repoID int64) (attachmentPaths []string, err error) { 641 // MariaDB has a performance bug: https://jira.mariadb.org/browse/MDEV-16289 642 // so here it uses "DELETE ... WHERE IN" with pre-queried IDs. 643 sess := db.GetEngine(ctx) 644 645 for { 646 issueIDs := make([]int64, 0, db.DefaultMaxInSize) 647 648 err := sess.Table(&Issue{}).Where("repo_id = ?", repoID).OrderBy("id").Limit(db.DefaultMaxInSize).Cols("id").Find(&issueIDs) 649 if err != nil { 650 return nil, err 651 } 652 653 if len(issueIDs) == 0 { 654 break 655 } 656 657 // Delete content histories 658 _, err = sess.In("issue_id", issueIDs).Delete(&ContentHistory{}) 659 if err != nil { 660 return nil, err 661 } 662 663 // Delete comments and attachments 664 _, err = sess.In("issue_id", issueIDs).Delete(&Comment{}) 665 if err != nil { 666 return nil, err 667 } 668 669 // Dependencies for issues in this repository 670 _, err = sess.In("issue_id", issueIDs).Delete(&IssueDependency{}) 671 if err != nil { 672 return nil, err 673 } 674 675 // Delete dependencies for issues in other repositories 676 _, err = sess.In("dependency_id", issueIDs).Delete(&IssueDependency{}) 677 if err != nil { 678 return nil, err 679 } 680 681 _, err = sess.In("issue_id", issueIDs).Delete(&IssueUser{}) 682 if err != nil { 683 return nil, err 684 } 685 686 _, err = sess.In("issue_id", issueIDs).Delete(&Reaction{}) 687 if err != nil { 688 return nil, err 689 } 690 691 _, err = sess.In("issue_id", issueIDs).Delete(&IssueWatch{}) 692 if err != nil { 693 return nil, err 694 } 695 696 _, err = sess.In("issue_id", issueIDs).Delete(&Stopwatch{}) 697 if err != nil { 698 return nil, err 699 } 700 701 _, err = sess.In("issue_id", issueIDs).Delete(&TrackedTime{}) 702 if err != nil { 703 return nil, err 704 } 705 706 _, err = sess.In("issue_id", issueIDs).Delete(&project_model.ProjectIssue{}) 707 if err != nil { 708 return nil, err 709 } 710 711 _, err = sess.In("dependent_issue_id", issueIDs).Delete(&Comment{}) 712 if err != nil { 713 return nil, err 714 } 715 716 var attachments []*repo_model.Attachment 717 err = sess.In("issue_id", issueIDs).Find(&attachments) 718 if err != nil { 719 return nil, err 720 } 721 722 for j := range attachments { 723 attachmentPaths = append(attachmentPaths, attachments[j].RelativePath()) 724 } 725 726 _, err = sess.In("issue_id", issueIDs).Delete(&repo_model.Attachment{}) 727 if err != nil { 728 return nil, err 729 } 730 731 _, err = sess.In("id", issueIDs).Delete(&Issue{}) 732 if err != nil { 733 return nil, err 734 } 735 } 736 737 return attachmentPaths, err 738 } 739 740 // DeleteOrphanedIssues delete issues without a repo 741 func DeleteOrphanedIssues(ctx context.Context) error { 742 var attachmentPaths []string 743 err := db.WithTx(ctx, func(ctx context.Context) error { 744 var ids []int64 745 746 if err := db.GetEngine(ctx).Table("issue").Distinct("issue.repo_id"). 747 Join("LEFT", "repository", "issue.repo_id=repository.id"). 748 Where(builder.IsNull{"repository.id"}).GroupBy("issue.repo_id"). 749 Find(&ids); err != nil { 750 return err 751 } 752 753 for i := range ids { 754 paths, err := DeleteIssuesByRepoID(ctx, ids[i]) 755 if err != nil { 756 return err 757 } 758 attachmentPaths = append(attachmentPaths, paths...) 759 } 760 761 return nil 762 }) 763 if err != nil { 764 return err 765 } 766 767 // Remove issue attachment files. 768 for i := range attachmentPaths { 769 system_model.RemoveAllWithNotice(ctx, "Delete issue attachment", attachmentPaths[i]) 770 } 771 return nil 772 }