code.gitea.io/gitea@v1.21.7/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 // UpdateIssueByAPI updates all allowed fields of given issue. 433 // If the issue status is changed a statusChangeComment is returned 434 // similarly if the title is changed the titleChanged bool is set to true 435 func UpdateIssueByAPI(ctx context.Context, issue *Issue, doer *user_model.User) (statusChangeComment *Comment, titleChanged bool, err error) { 436 ctx, committer, err := db.TxContext(ctx) 437 if err != nil { 438 return nil, false, err 439 } 440 defer committer.Close() 441 442 if err := issue.LoadRepo(ctx); err != nil { 443 return nil, false, fmt.Errorf("loadRepo: %w", err) 444 } 445 446 // Reload the issue 447 currentIssue, err := GetIssueByID(ctx, issue.ID) 448 if err != nil { 449 return nil, false, err 450 } 451 452 if _, err := db.GetEngine(ctx).ID(issue.ID).Cols( 453 "name", "content", "milestone_id", "priority", 454 "deadline_unix", "updated_unix", "is_locked"). 455 Update(issue); err != nil { 456 return nil, false, err 457 } 458 459 titleChanged = currentIssue.Title != issue.Title 460 if titleChanged { 461 opts := &CreateCommentOptions{ 462 Type: CommentTypeChangeTitle, 463 Doer: doer, 464 Repo: issue.Repo, 465 Issue: issue, 466 OldTitle: currentIssue.Title, 467 NewTitle: issue.Title, 468 } 469 _, err := CreateComment(ctx, opts) 470 if err != nil { 471 return nil, false, fmt.Errorf("createComment: %w", err) 472 } 473 } 474 475 if currentIssue.IsClosed != issue.IsClosed { 476 statusChangeComment, err = doChangeIssueStatus(ctx, issue, doer, false) 477 if err != nil { 478 return nil, false, err 479 } 480 } 481 482 if err := issue.AddCrossReferences(ctx, doer, true); err != nil { 483 return nil, false, err 484 } 485 return statusChangeComment, titleChanged, committer.Commit() 486 } 487 488 // UpdateIssueDeadline updates an issue deadline and adds comments. Setting a deadline to 0 means deleting it. 489 func UpdateIssueDeadline(ctx context.Context, issue *Issue, deadlineUnix timeutil.TimeStamp, doer *user_model.User) (err error) { 490 // if the deadline hasn't changed do nothing 491 if issue.DeadlineUnix == deadlineUnix { 492 return nil 493 } 494 ctx, committer, err := db.TxContext(ctx) 495 if err != nil { 496 return err 497 } 498 defer committer.Close() 499 500 // Update the deadline 501 if err = UpdateIssueCols(ctx, &Issue{ID: issue.ID, DeadlineUnix: deadlineUnix}, "deadline_unix"); err != nil { 502 return err 503 } 504 505 // Make the comment 506 if _, err = createDeadlineComment(ctx, doer, issue, deadlineUnix); err != nil { 507 return fmt.Errorf("createRemovedDueDateComment: %w", err) 508 } 509 510 return committer.Commit() 511 } 512 513 // DeleteInIssue delete records in beans with external key issue_id = ? 514 func DeleteInIssue(ctx context.Context, issueID int64, beans ...any) error { 515 e := db.GetEngine(ctx) 516 for _, bean := range beans { 517 if _, err := e.In("issue_id", issueID).Delete(bean); err != nil { 518 return err 519 } 520 } 521 return nil 522 } 523 524 // FindAndUpdateIssueMentions finds users mentioned in the given content string, and saves them in the database. 525 func FindAndUpdateIssueMentions(ctx context.Context, issue *Issue, doer *user_model.User, content string) (mentions []*user_model.User, err error) { 526 rawMentions := references.FindAllMentionsMarkdown(content) 527 mentions, err = ResolveIssueMentionsByVisibility(ctx, issue, doer, rawMentions) 528 if err != nil { 529 return nil, fmt.Errorf("UpdateIssueMentions [%d]: %w", issue.ID, err) 530 } 531 if err = UpdateIssueMentions(ctx, issue.ID, mentions); err != nil { 532 return nil, fmt.Errorf("UpdateIssueMentions [%d]: %w", issue.ID, err) 533 } 534 return mentions, err 535 } 536 537 // ResolveIssueMentionsByVisibility returns the users mentioned in an issue, removing those that 538 // don't have access to reading it. Teams are expanded into their users, but organizations are ignored. 539 func ResolveIssueMentionsByVisibility(ctx context.Context, issue *Issue, doer *user_model.User, mentions []string) (users []*user_model.User, err error) { 540 if len(mentions) == 0 { 541 return nil, nil 542 } 543 if err = issue.LoadRepo(ctx); err != nil { 544 return nil, err 545 } 546 547 resolved := make(map[string]bool, 10) 548 var mentionTeams []string 549 550 if err := issue.Repo.LoadOwner(ctx); err != nil { 551 return nil, err 552 } 553 554 repoOwnerIsOrg := issue.Repo.Owner.IsOrganization() 555 if repoOwnerIsOrg { 556 mentionTeams = make([]string, 0, 5) 557 } 558 559 resolved[doer.LowerName] = true 560 for _, name := range mentions { 561 name := strings.ToLower(name) 562 if _, ok := resolved[name]; ok { 563 continue 564 } 565 if repoOwnerIsOrg && strings.Contains(name, "/") { 566 names := strings.Split(name, "/") 567 if len(names) < 2 || names[0] != issue.Repo.Owner.LowerName { 568 continue 569 } 570 mentionTeams = append(mentionTeams, names[1]) 571 resolved[name] = true 572 } else { 573 resolved[name] = false 574 } 575 } 576 577 if issue.Repo.Owner.IsOrganization() && len(mentionTeams) > 0 { 578 teams := make([]*organization.Team, 0, len(mentionTeams)) 579 if err := db.GetEngine(ctx). 580 Join("INNER", "team_repo", "team_repo.team_id = team.id"). 581 Where("team_repo.repo_id=?", issue.Repo.ID). 582 In("team.lower_name", mentionTeams). 583 Find(&teams); err != nil { 584 return nil, fmt.Errorf("find mentioned teams: %w", err) 585 } 586 if len(teams) != 0 { 587 checked := make([]int64, 0, len(teams)) 588 unittype := unit.TypeIssues 589 if issue.IsPull { 590 unittype = unit.TypePullRequests 591 } 592 for _, team := range teams { 593 if team.AccessMode >= perm.AccessModeAdmin { 594 checked = append(checked, team.ID) 595 resolved[issue.Repo.Owner.LowerName+"/"+team.LowerName] = true 596 continue 597 } 598 has, err := db.GetEngine(ctx).Get(&organization.TeamUnit{OrgID: issue.Repo.Owner.ID, TeamID: team.ID, Type: unittype}) 599 if err != nil { 600 return nil, fmt.Errorf("get team units (%d): %w", team.ID, err) 601 } 602 if has { 603 checked = append(checked, team.ID) 604 resolved[issue.Repo.Owner.LowerName+"/"+team.LowerName] = true 605 } 606 } 607 if len(checked) != 0 { 608 teamusers := make([]*user_model.User, 0, 20) 609 if err := db.GetEngine(ctx). 610 Join("INNER", "team_user", "team_user.uid = `user`.id"). 611 In("`team_user`.team_id", checked). 612 And("`user`.is_active = ?", true). 613 And("`user`.prohibit_login = ?", false). 614 Find(&teamusers); err != nil { 615 return nil, fmt.Errorf("get teams users: %w", err) 616 } 617 if len(teamusers) > 0 { 618 users = make([]*user_model.User, 0, len(teamusers)) 619 for _, user := range teamusers { 620 if already, ok := resolved[user.LowerName]; !ok || !already { 621 users = append(users, user) 622 resolved[user.LowerName] = true 623 } 624 } 625 } 626 } 627 } 628 } 629 630 // Remove names already in the list to avoid querying the database if pending names remain 631 mentionUsers := make([]string, 0, len(resolved)) 632 for name, already := range resolved { 633 if !already { 634 mentionUsers = append(mentionUsers, name) 635 } 636 } 637 if len(mentionUsers) == 0 { 638 return users, err 639 } 640 641 if users == nil { 642 users = make([]*user_model.User, 0, len(mentionUsers)) 643 } 644 645 unchecked := make([]*user_model.User, 0, len(mentionUsers)) 646 if err := db.GetEngine(ctx). 647 Where("`user`.is_active = ?", true). 648 And("`user`.prohibit_login = ?", false). 649 In("`user`.lower_name", mentionUsers). 650 Find(&unchecked); err != nil { 651 return nil, fmt.Errorf("find mentioned users: %w", err) 652 } 653 for _, user := range unchecked { 654 if already := resolved[user.LowerName]; already || user.IsOrganization() { 655 continue 656 } 657 // Normal users must have read access to the referencing issue 658 perm, err := access_model.GetUserRepoPermission(ctx, issue.Repo, user) 659 if err != nil { 660 return nil, fmt.Errorf("GetUserRepoPermission [%d]: %w", user.ID, err) 661 } 662 if !perm.CanReadIssuesOrPulls(issue.IsPull) { 663 continue 664 } 665 users = append(users, user) 666 } 667 668 return users, err 669 } 670 671 // UpdateIssuesMigrationsByType updates all migrated repositories' issues from gitServiceType to replace originalAuthorID to posterID 672 func UpdateIssuesMigrationsByType(ctx context.Context, gitServiceType api.GitServiceType, originalAuthorID string, posterID int64) error { 673 _, err := db.GetEngine(ctx).Table("issue"). 674 Where("repo_id IN (SELECT id FROM repository WHERE original_service_type = ?)", gitServiceType). 675 And("original_author_id = ?", originalAuthorID). 676 Update(map[string]any{ 677 "poster_id": posterID, 678 "original_author": "", 679 "original_author_id": 0, 680 }) 681 return err 682 } 683 684 // UpdateReactionsMigrationsByType updates all migrated repositories' reactions from gitServiceType to replace originalAuthorID to posterID 685 func UpdateReactionsMigrationsByType(ctx context.Context, gitServiceType api.GitServiceType, originalAuthorID string, userID int64) error { 686 _, err := db.GetEngine(ctx).Table("reaction"). 687 Where("original_author_id = ?", originalAuthorID). 688 And(migratedIssueCond(gitServiceType)). 689 Update(map[string]any{ 690 "user_id": userID, 691 "original_author": "", 692 "original_author_id": 0, 693 }) 694 return err 695 } 696 697 // DeleteIssuesByRepoID deletes issues by repositories id 698 func DeleteIssuesByRepoID(ctx context.Context, repoID int64) (attachmentPaths []string, err error) { 699 // MariaDB has a performance bug: https://jira.mariadb.org/browse/MDEV-16289 700 // so here it uses "DELETE ... WHERE IN" with pre-queried IDs. 701 sess := db.GetEngine(ctx) 702 703 for { 704 issueIDs := make([]int64, 0, db.DefaultMaxInSize) 705 706 err := sess.Table(&Issue{}).Where("repo_id = ?", repoID).OrderBy("id").Limit(db.DefaultMaxInSize).Cols("id").Find(&issueIDs) 707 if err != nil { 708 return nil, err 709 } 710 711 if len(issueIDs) == 0 { 712 break 713 } 714 715 // Delete content histories 716 _, err = sess.In("issue_id", issueIDs).Delete(&ContentHistory{}) 717 if err != nil { 718 return nil, err 719 } 720 721 // Delete comments and attachments 722 _, err = sess.In("issue_id", issueIDs).Delete(&Comment{}) 723 if err != nil { 724 return nil, err 725 } 726 727 // Dependencies for issues in this repository 728 _, err = sess.In("issue_id", issueIDs).Delete(&IssueDependency{}) 729 if err != nil { 730 return nil, err 731 } 732 733 // Delete dependencies for issues in other repositories 734 _, err = sess.In("dependency_id", issueIDs).Delete(&IssueDependency{}) 735 if err != nil { 736 return nil, err 737 } 738 739 _, err = sess.In("issue_id", issueIDs).Delete(&IssueUser{}) 740 if err != nil { 741 return nil, err 742 } 743 744 _, err = sess.In("issue_id", issueIDs).Delete(&Reaction{}) 745 if err != nil { 746 return nil, err 747 } 748 749 _, err = sess.In("issue_id", issueIDs).Delete(&IssueWatch{}) 750 if err != nil { 751 return nil, err 752 } 753 754 _, err = sess.In("issue_id", issueIDs).Delete(&Stopwatch{}) 755 if err != nil { 756 return nil, err 757 } 758 759 _, err = sess.In("issue_id", issueIDs).Delete(&TrackedTime{}) 760 if err != nil { 761 return nil, err 762 } 763 764 _, err = sess.In("issue_id", issueIDs).Delete(&project_model.ProjectIssue{}) 765 if err != nil { 766 return nil, err 767 } 768 769 _, err = sess.In("dependent_issue_id", issueIDs).Delete(&Comment{}) 770 if err != nil { 771 return nil, err 772 } 773 774 var attachments []*repo_model.Attachment 775 err = sess.In("issue_id", issueIDs).Find(&attachments) 776 if err != nil { 777 return nil, err 778 } 779 780 for j := range attachments { 781 attachmentPaths = append(attachmentPaths, attachments[j].RelativePath()) 782 } 783 784 _, err = sess.In("issue_id", issueIDs).Delete(&repo_model.Attachment{}) 785 if err != nil { 786 return nil, err 787 } 788 789 _, err = sess.In("id", issueIDs).Delete(&Issue{}) 790 if err != nil { 791 return nil, err 792 } 793 } 794 795 return attachmentPaths, err 796 } 797 798 // DeleteOrphanedIssues delete issues without a repo 799 func DeleteOrphanedIssues(ctx context.Context) error { 800 var attachmentPaths []string 801 err := db.WithTx(ctx, func(ctx context.Context) error { 802 var ids []int64 803 804 if err := db.GetEngine(ctx).Table("issue").Distinct("issue.repo_id"). 805 Join("LEFT", "repository", "issue.repo_id=repository.id"). 806 Where(builder.IsNull{"repository.id"}).GroupBy("issue.repo_id"). 807 Find(&ids); err != nil { 808 return err 809 } 810 811 for i := range ids { 812 paths, err := DeleteIssuesByRepoID(ctx, ids[i]) 813 if err != nil { 814 return err 815 } 816 attachmentPaths = append(attachmentPaths, paths...) 817 } 818 819 return nil 820 }) 821 if err != nil { 822 return err 823 } 824 825 // Remove issue attachment files. 826 for i := range attachmentPaths { 827 system_model.RemoveAllWithNotice(ctx, "Delete issue attachment", attachmentPaths[i]) 828 } 829 return nil 830 }