code.gitea.io/gitea@v1.21.7/models/activities/action.go (about) 1 // Copyright 2014 The Gogs Authors. All rights reserved. 2 // Copyright 2019 The Gitea Authors. All rights reserved. 3 // SPDX-License-Identifier: MIT 4 5 package activities 6 7 import ( 8 "context" 9 "fmt" 10 "net/url" 11 "path" 12 "strconv" 13 "strings" 14 "time" 15 16 "code.gitea.io/gitea/models/db" 17 issues_model "code.gitea.io/gitea/models/issues" 18 "code.gitea.io/gitea/models/organization" 19 access_model "code.gitea.io/gitea/models/perm/access" 20 repo_model "code.gitea.io/gitea/models/repo" 21 "code.gitea.io/gitea/models/unit" 22 user_model "code.gitea.io/gitea/models/user" 23 "code.gitea.io/gitea/modules/base" 24 "code.gitea.io/gitea/modules/git" 25 "code.gitea.io/gitea/modules/log" 26 "code.gitea.io/gitea/modules/setting" 27 "code.gitea.io/gitea/modules/structs" 28 "code.gitea.io/gitea/modules/timeutil" 29 30 "xorm.io/builder" 31 "xorm.io/xorm/schemas" 32 ) 33 34 // ActionType represents the type of an action. 35 type ActionType int 36 37 // Possible action types. 38 const ( 39 ActionCreateRepo ActionType = iota + 1 // 1 40 ActionRenameRepo // 2 41 ActionStarRepo // 3 42 ActionWatchRepo // 4 43 ActionCommitRepo // 5 44 ActionCreateIssue // 6 45 ActionCreatePullRequest // 7 46 ActionTransferRepo // 8 47 ActionPushTag // 9 48 ActionCommentIssue // 10 49 ActionMergePullRequest // 11 50 ActionCloseIssue // 12 51 ActionReopenIssue // 13 52 ActionClosePullRequest // 14 53 ActionReopenPullRequest // 15 54 ActionDeleteTag // 16 55 ActionDeleteBranch // 17 56 ActionMirrorSyncPush // 18 57 ActionMirrorSyncCreate // 19 58 ActionMirrorSyncDelete // 20 59 ActionApprovePullRequest // 21 60 ActionRejectPullRequest // 22 61 ActionCommentPull // 23 62 ActionPublishRelease // 24 63 ActionPullReviewDismissed // 25 64 ActionPullRequestReadyForReview // 26 65 ActionAutoMergePullRequest // 27 66 ) 67 68 func (at ActionType) String() string { 69 switch at { 70 case ActionCreateRepo: 71 return "create_repo" 72 case ActionRenameRepo: 73 return "rename_repo" 74 case ActionStarRepo: 75 return "star_repo" 76 case ActionWatchRepo: 77 return "watch_repo" 78 case ActionCommitRepo: 79 return "commit_repo" 80 case ActionCreateIssue: 81 return "create_issue" 82 case ActionCreatePullRequest: 83 return "create_pull_request" 84 case ActionTransferRepo: 85 return "transfer_repo" 86 case ActionPushTag: 87 return "push_tag" 88 case ActionCommentIssue: 89 return "comment_issue" 90 case ActionMergePullRequest: 91 return "merge_pull_request" 92 case ActionCloseIssue: 93 return "close_issue" 94 case ActionReopenIssue: 95 return "reopen_issue" 96 case ActionClosePullRequest: 97 return "close_pull_request" 98 case ActionReopenPullRequest: 99 return "reopen_pull_request" 100 case ActionDeleteTag: 101 return "delete_tag" 102 case ActionDeleteBranch: 103 return "delete_branch" 104 case ActionMirrorSyncPush: 105 return "mirror_sync_push" 106 case ActionMirrorSyncCreate: 107 return "mirror_sync_create" 108 case ActionMirrorSyncDelete: 109 return "mirror_sync_delete" 110 case ActionApprovePullRequest: 111 return "approve_pull_request" 112 case ActionRejectPullRequest: 113 return "reject_pull_request" 114 case ActionCommentPull: 115 return "comment_pull" 116 case ActionPublishRelease: 117 return "publish_release" 118 case ActionPullReviewDismissed: 119 return "pull_review_dismissed" 120 case ActionPullRequestReadyForReview: 121 return "pull_request_ready_for_review" 122 case ActionAutoMergePullRequest: 123 return "auto_merge_pull_request" 124 default: 125 return "action-" + strconv.Itoa(int(at)) 126 } 127 } 128 129 func (at ActionType) InActions(actions ...string) bool { 130 for _, action := range actions { 131 if action == at.String() { 132 return true 133 } 134 } 135 return false 136 } 137 138 // Action represents user operation type and other information to 139 // repository. It implemented interface base.Actioner so that can be 140 // used in template render. 141 type Action struct { 142 ID int64 `xorm:"pk autoincr"` 143 UserID int64 `xorm:"INDEX"` // Receiver user id. 144 OpType ActionType 145 ActUserID int64 // Action user id. 146 ActUser *user_model.User `xorm:"-"` 147 RepoID int64 148 Repo *repo_model.Repository `xorm:"-"` 149 CommentID int64 `xorm:"INDEX"` 150 Comment *issues_model.Comment `xorm:"-"` 151 IsDeleted bool `xorm:"NOT NULL DEFAULT false"` 152 RefName string 153 IsPrivate bool `xorm:"NOT NULL DEFAULT false"` 154 Content string `xorm:"TEXT"` 155 CreatedUnix timeutil.TimeStamp `xorm:"created"` 156 } 157 158 func init() { 159 db.RegisterModel(new(Action)) 160 } 161 162 // TableIndices implements xorm's TableIndices interface 163 func (a *Action) TableIndices() []*schemas.Index { 164 repoIndex := schemas.NewIndex("r_u_d", schemas.IndexType) 165 repoIndex.AddColumn("repo_id", "user_id", "is_deleted") 166 167 actUserIndex := schemas.NewIndex("au_r_c_u_d", schemas.IndexType) 168 actUserIndex.AddColumn("act_user_id", "repo_id", "created_unix", "user_id", "is_deleted") 169 170 cudIndex := schemas.NewIndex("c_u_d", schemas.IndexType) 171 cudIndex.AddColumn("created_unix", "user_id", "is_deleted") 172 173 indices := []*schemas.Index{actUserIndex, repoIndex, cudIndex} 174 175 return indices 176 } 177 178 // GetOpType gets the ActionType of this action. 179 func (a *Action) GetOpType() ActionType { 180 return a.OpType 181 } 182 183 // LoadActUser loads a.ActUser 184 func (a *Action) LoadActUser(ctx context.Context) { 185 if a.ActUser != nil { 186 return 187 } 188 var err error 189 a.ActUser, err = user_model.GetUserByID(ctx, a.ActUserID) 190 if err == nil { 191 return 192 } else if user_model.IsErrUserNotExist(err) { 193 a.ActUser = user_model.NewGhostUser() 194 } else { 195 log.Error("GetUserByID(%d): %v", a.ActUserID, err) 196 } 197 } 198 199 func (a *Action) loadRepo(ctx context.Context) { 200 if a.Repo != nil { 201 return 202 } 203 var err error 204 a.Repo, err = repo_model.GetRepositoryByID(ctx, a.RepoID) 205 if err != nil { 206 log.Error("repo_model.GetRepositoryByID(%d): %v", a.RepoID, err) 207 } 208 } 209 210 // GetActFullName gets the action's user full name. 211 func (a *Action) GetActFullName(ctx context.Context) string { 212 a.LoadActUser(ctx) 213 return a.ActUser.FullName 214 } 215 216 // GetActUserName gets the action's user name. 217 func (a *Action) GetActUserName(ctx context.Context) string { 218 a.LoadActUser(ctx) 219 return a.ActUser.Name 220 } 221 222 // ShortActUserName gets the action's user name trimmed to max 20 223 // chars. 224 func (a *Action) ShortActUserName(ctx context.Context) string { 225 return base.EllipsisString(a.GetActUserName(ctx), 20) 226 } 227 228 // GetDisplayName gets the action's display name based on DEFAULT_SHOW_FULL_NAME, or falls back to the username if it is blank. 229 func (a *Action) GetDisplayName(ctx context.Context) string { 230 if setting.UI.DefaultShowFullName { 231 trimmedFullName := strings.TrimSpace(a.GetActFullName(ctx)) 232 if len(trimmedFullName) > 0 { 233 return trimmedFullName 234 } 235 } 236 return a.ShortActUserName(ctx) 237 } 238 239 // GetDisplayNameTitle gets the action's display name used for the title (tooltip) based on DEFAULT_SHOW_FULL_NAME 240 func (a *Action) GetDisplayNameTitle(ctx context.Context) string { 241 if setting.UI.DefaultShowFullName { 242 return a.ShortActUserName(ctx) 243 } 244 return a.GetActFullName(ctx) 245 } 246 247 // GetRepoUserName returns the name of the action repository owner. 248 func (a *Action) GetRepoUserName(ctx context.Context) string { 249 a.loadRepo(ctx) 250 return a.Repo.OwnerName 251 } 252 253 // ShortRepoUserName returns the name of the action repository owner 254 // trimmed to max 20 chars. 255 func (a *Action) ShortRepoUserName(ctx context.Context) string { 256 return base.EllipsisString(a.GetRepoUserName(ctx), 20) 257 } 258 259 // GetRepoName returns the name of the action repository. 260 func (a *Action) GetRepoName(ctx context.Context) string { 261 a.loadRepo(ctx) 262 return a.Repo.Name 263 } 264 265 // ShortRepoName returns the name of the action repository 266 // trimmed to max 33 chars. 267 func (a *Action) ShortRepoName(ctx context.Context) string { 268 return base.EllipsisString(a.GetRepoName(ctx), 33) 269 } 270 271 // GetRepoPath returns the virtual path to the action repository. 272 func (a *Action) GetRepoPath(ctx context.Context) string { 273 return path.Join(a.GetRepoUserName(ctx), a.GetRepoName(ctx)) 274 } 275 276 // ShortRepoPath returns the virtual path to the action repository 277 // trimmed to max 20 + 1 + 33 chars. 278 func (a *Action) ShortRepoPath(ctx context.Context) string { 279 return path.Join(a.ShortRepoUserName(ctx), a.ShortRepoName(ctx)) 280 } 281 282 // GetRepoLink returns relative link to action repository. 283 func (a *Action) GetRepoLink(ctx context.Context) string { 284 // path.Join will skip empty strings 285 return path.Join(setting.AppSubURL, "/", url.PathEscape(a.GetRepoUserName(ctx)), url.PathEscape(a.GetRepoName(ctx))) 286 } 287 288 // GetRepoAbsoluteLink returns the absolute link to action repository. 289 func (a *Action) GetRepoAbsoluteLink(ctx context.Context) string { 290 return setting.AppURL + url.PathEscape(a.GetRepoUserName(ctx)) + "/" + url.PathEscape(a.GetRepoName(ctx)) 291 } 292 293 // GetCommentHTMLURL returns link to action comment. 294 func (a *Action) GetCommentHTMLURL(ctx context.Context) string { 295 return a.getCommentHTMLURL(ctx) 296 } 297 298 func (a *Action) loadComment(ctx context.Context) (err error) { 299 if a.CommentID == 0 || a.Comment != nil { 300 return nil 301 } 302 a.Comment, err = issues_model.GetCommentByID(ctx, a.CommentID) 303 return err 304 } 305 306 func (a *Action) getCommentHTMLURL(ctx context.Context) string { 307 if a == nil { 308 return "#" 309 } 310 _ = a.loadComment(ctx) 311 if a.Comment != nil { 312 return a.Comment.HTMLURL(ctx) 313 } 314 if len(a.GetIssueInfos()) == 0 { 315 return "#" 316 } 317 // Return link to issue 318 issueIDString := a.GetIssueInfos()[0] 319 issueID, err := strconv.ParseInt(issueIDString, 10, 64) 320 if err != nil { 321 return "#" 322 } 323 324 issue, err := issues_model.GetIssueByID(ctx, issueID) 325 if err != nil { 326 return "#" 327 } 328 329 if err = issue.LoadRepo(ctx); err != nil { 330 return "#" 331 } 332 333 return issue.HTMLURL() 334 } 335 336 // GetCommentLink returns link to action comment. 337 func (a *Action) GetCommentLink(ctx context.Context) string { 338 return a.getCommentLink(ctx) 339 } 340 341 func (a *Action) getCommentLink(ctx context.Context) string { 342 if a == nil { 343 return "#" 344 } 345 _ = a.loadComment(ctx) 346 if a.Comment != nil { 347 return a.Comment.Link(ctx) 348 } 349 if len(a.GetIssueInfos()) == 0 { 350 return "#" 351 } 352 // Return link to issue 353 issueIDString := a.GetIssueInfos()[0] 354 issueID, err := strconv.ParseInt(issueIDString, 10, 64) 355 if err != nil { 356 return "#" 357 } 358 359 issue, err := issues_model.GetIssueByID(ctx, issueID) 360 if err != nil { 361 return "#" 362 } 363 364 if err = issue.LoadRepo(ctx); err != nil { 365 return "#" 366 } 367 368 return issue.Link() 369 } 370 371 // GetBranch returns the action's repository branch. 372 func (a *Action) GetBranch() string { 373 return strings.TrimPrefix(a.RefName, git.BranchPrefix) 374 } 375 376 // GetRefLink returns the action's ref link. 377 func (a *Action) GetRefLink(ctx context.Context) string { 378 return git.RefURL(a.GetRepoLink(ctx), a.RefName) 379 } 380 381 // GetTag returns the action's repository tag. 382 func (a *Action) GetTag() string { 383 return strings.TrimPrefix(a.RefName, git.TagPrefix) 384 } 385 386 // GetContent returns the action's content. 387 func (a *Action) GetContent() string { 388 return a.Content 389 } 390 391 // GetCreate returns the action creation time. 392 func (a *Action) GetCreate() time.Time { 393 return a.CreatedUnix.AsTime() 394 } 395 396 // GetIssueInfos returns a list of issues associated with 397 // the action. 398 func (a *Action) GetIssueInfos() []string { 399 return strings.SplitN(a.Content, "|", 3) 400 } 401 402 // GetIssueTitle returns the title of first issue associated with the action. 403 func (a *Action) GetIssueTitle(ctx context.Context) string { 404 index, _ := strconv.ParseInt(a.GetIssueInfos()[0], 10, 64) 405 issue, err := issues_model.GetIssueByIndex(ctx, a.RepoID, index) 406 if err != nil { 407 log.Error("GetIssueByIndex: %v", err) 408 return "500 when get issue" 409 } 410 return issue.Title 411 } 412 413 // GetIssueContent returns the content of first issue associated with 414 // this action. 415 func (a *Action) GetIssueContent(ctx context.Context) string { 416 index, _ := strconv.ParseInt(a.GetIssueInfos()[0], 10, 64) 417 issue, err := issues_model.GetIssueByIndex(ctx, a.RepoID, index) 418 if err != nil { 419 log.Error("GetIssueByIndex: %v", err) 420 return "500 when get issue" 421 } 422 return issue.Content 423 } 424 425 // GetFeedsOptions options for retrieving feeds 426 type GetFeedsOptions struct { 427 db.ListOptions 428 RequestedUser *user_model.User // the user we want activity for 429 RequestedTeam *organization.Team // the team we want activity for 430 RequestedRepo *repo_model.Repository // the repo we want activity for 431 Actor *user_model.User // the user viewing the activity 432 IncludePrivate bool // include private actions 433 OnlyPerformedBy bool // only actions performed by requested user 434 IncludeDeleted bool // include deleted actions 435 Date string // the day we want activity for: YYYY-MM-DD 436 } 437 438 // GetFeeds returns actions according to the provided options 439 func GetFeeds(ctx context.Context, opts GetFeedsOptions) (ActionList, int64, error) { 440 if opts.RequestedUser == nil && opts.RequestedTeam == nil && opts.RequestedRepo == nil { 441 return nil, 0, fmt.Errorf("need at least one of these filters: RequestedUser, RequestedTeam, RequestedRepo") 442 } 443 444 cond, err := activityQueryCondition(ctx, opts) 445 if err != nil { 446 return nil, 0, err 447 } 448 449 sess := db.GetEngine(ctx).Where(cond). 450 Select("`action`.*"). // this line will avoid select other joined table's columns 451 Join("INNER", "repository", "`repository`.id = `action`.repo_id") 452 453 opts.SetDefaultValues() 454 sess = db.SetSessionPagination(sess, &opts) 455 456 actions := make([]*Action, 0, opts.PageSize) 457 count, err := sess.Desc("`action`.created_unix").FindAndCount(&actions) 458 if err != nil { 459 return nil, 0, fmt.Errorf("FindAndCount: %w", err) 460 } 461 462 if err := ActionList(actions).loadAttributes(ctx); err != nil { 463 return nil, 0, fmt.Errorf("LoadAttributes: %w", err) 464 } 465 466 return actions, count, nil 467 } 468 469 // ActivityReadable return whether doer can read activities of user 470 func ActivityReadable(user, doer *user_model.User) bool { 471 return !user.KeepActivityPrivate || 472 doer != nil && (doer.IsAdmin || user.ID == doer.ID) 473 } 474 475 func activityQueryCondition(ctx context.Context, opts GetFeedsOptions) (builder.Cond, error) { 476 cond := builder.NewCond() 477 478 if opts.RequestedTeam != nil && opts.RequestedUser == nil { 479 org, err := user_model.GetUserByID(ctx, opts.RequestedTeam.OrgID) 480 if err != nil { 481 return nil, err 482 } 483 opts.RequestedUser = org 484 } 485 486 // check activity visibility for actor ( similar to activityReadable() ) 487 if opts.Actor == nil { 488 cond = cond.And(builder.In("act_user_id", 489 builder.Select("`user`.id").Where( 490 builder.Eq{"keep_activity_private": false, "visibility": structs.VisibleTypePublic}, 491 ).From("`user`"), 492 )) 493 } else if !opts.Actor.IsAdmin { 494 uidCond := builder.Select("`user`.id").From("`user`").Where( 495 builder.Eq{"keep_activity_private": false}. 496 And(builder.In("visibility", structs.VisibleTypePublic, structs.VisibleTypeLimited))). 497 Or(builder.Eq{"id": opts.Actor.ID}) 498 499 if opts.RequestedUser != nil { 500 if opts.RequestedUser.IsOrganization() { 501 // An organization can always see the activities whose `act_user_id` is the same as its id. 502 uidCond = uidCond.Or(builder.Eq{"id": opts.RequestedUser.ID}) 503 } else { 504 // A user can always see the activities of the organizations to which the user belongs. 505 uidCond = uidCond.Or( 506 builder.Eq{"type": user_model.UserTypeOrganization}. 507 And(builder.In("`user`.id", builder.Select("org_id"). 508 Where(builder.Eq{"uid": opts.RequestedUser.ID}). 509 From("team_user"))), 510 ) 511 } 512 } 513 514 cond = cond.And(builder.In("act_user_id", uidCond)) 515 } 516 517 // check readable repositories by doer/actor 518 if opts.Actor == nil || !opts.Actor.IsAdmin { 519 cond = cond.And(builder.In("repo_id", repo_model.AccessibleRepoIDsQuery(opts.Actor))) 520 } 521 522 if opts.RequestedRepo != nil { 523 cond = cond.And(builder.Eq{"repo_id": opts.RequestedRepo.ID}) 524 } 525 526 if opts.RequestedTeam != nil { 527 env := organization.OrgFromUser(opts.RequestedUser).AccessibleTeamReposEnv(opts.RequestedTeam) 528 teamRepoIDs, err := env.RepoIDs(1, opts.RequestedUser.NumRepos) 529 if err != nil { 530 return nil, fmt.Errorf("GetTeamRepositories: %w", err) 531 } 532 cond = cond.And(builder.In("repo_id", teamRepoIDs)) 533 } 534 535 if opts.RequestedUser != nil { 536 cond = cond.And(builder.Eq{"user_id": opts.RequestedUser.ID}) 537 538 if opts.OnlyPerformedBy { 539 cond = cond.And(builder.Eq{"act_user_id": opts.RequestedUser.ID}) 540 } 541 } 542 543 if !opts.IncludePrivate { 544 cond = cond.And(builder.Eq{"`action`.is_private": false}) 545 } 546 if !opts.IncludeDeleted { 547 cond = cond.And(builder.Eq{"is_deleted": false}) 548 } 549 550 if opts.Date != "" { 551 dateLow, err := time.ParseInLocation("2006-01-02", opts.Date, setting.DefaultUILocation) 552 if err != nil { 553 log.Warn("Unable to parse %s, filter not applied: %v", opts.Date, err) 554 } else { 555 dateHigh := dateLow.Add(86399000000000) // 23h59m59s 556 557 cond = cond.And(builder.Gte{"`action`.created_unix": dateLow.Unix()}) 558 cond = cond.And(builder.Lte{"`action`.created_unix": dateHigh.Unix()}) 559 } 560 } 561 562 return cond, nil 563 } 564 565 // DeleteOldActions deletes all old actions from database. 566 func DeleteOldActions(ctx context.Context, olderThan time.Duration) (err error) { 567 if olderThan <= 0 { 568 return nil 569 } 570 571 _, err = db.GetEngine(ctx).Where("created_unix < ?", time.Now().Add(-olderThan).Unix()).Delete(&Action{}) 572 return err 573 } 574 575 // NotifyWatchers creates batch of actions for every watcher. 576 func NotifyWatchers(ctx context.Context, actions ...*Action) error { 577 var watchers []*repo_model.Watch 578 var repo *repo_model.Repository 579 var err error 580 var permCode []bool 581 var permIssue []bool 582 var permPR []bool 583 584 e := db.GetEngine(ctx) 585 586 for _, act := range actions { 587 repoChanged := repo == nil || repo.ID != act.RepoID 588 589 if repoChanged { 590 // Add feeds for user self and all watchers. 591 watchers, err = repo_model.GetWatchers(ctx, act.RepoID) 592 if err != nil { 593 return fmt.Errorf("get watchers: %w", err) 594 } 595 } 596 597 // Add feed for actioner. 598 act.UserID = act.ActUserID 599 if _, err = e.Insert(act); err != nil { 600 return fmt.Errorf("insert new actioner: %w", err) 601 } 602 603 if repoChanged { 604 act.loadRepo(ctx) 605 repo = act.Repo 606 607 // check repo owner exist. 608 if err := act.Repo.LoadOwner(ctx); err != nil { 609 return fmt.Errorf("can't get repo owner: %w", err) 610 } 611 } else if act.Repo == nil { 612 act.Repo = repo 613 } 614 615 // Add feed for organization 616 if act.Repo.Owner.IsOrganization() && act.ActUserID != act.Repo.Owner.ID { 617 act.ID = 0 618 act.UserID = act.Repo.Owner.ID 619 if err = db.Insert(ctx, act); err != nil { 620 return fmt.Errorf("insert new actioner: %w", err) 621 } 622 } 623 624 if repoChanged { 625 permCode = make([]bool, len(watchers)) 626 permIssue = make([]bool, len(watchers)) 627 permPR = make([]bool, len(watchers)) 628 for i, watcher := range watchers { 629 user, err := user_model.GetUserByID(ctx, watcher.UserID) 630 if err != nil { 631 permCode[i] = false 632 permIssue[i] = false 633 permPR[i] = false 634 continue 635 } 636 perm, err := access_model.GetUserRepoPermission(ctx, repo, user) 637 if err != nil { 638 permCode[i] = false 639 permIssue[i] = false 640 permPR[i] = false 641 continue 642 } 643 permCode[i] = perm.CanRead(unit.TypeCode) 644 permIssue[i] = perm.CanRead(unit.TypeIssues) 645 permPR[i] = perm.CanRead(unit.TypePullRequests) 646 } 647 } 648 649 for i, watcher := range watchers { 650 if act.ActUserID == watcher.UserID { 651 continue 652 } 653 act.ID = 0 654 act.UserID = watcher.UserID 655 act.Repo.Units = nil 656 657 switch act.OpType { 658 case ActionCommitRepo, ActionPushTag, ActionDeleteTag, ActionPublishRelease, ActionDeleteBranch: 659 if !permCode[i] { 660 continue 661 } 662 case ActionCreateIssue, ActionCommentIssue, ActionCloseIssue, ActionReopenIssue: 663 if !permIssue[i] { 664 continue 665 } 666 case ActionCreatePullRequest, ActionCommentPull, ActionMergePullRequest, ActionClosePullRequest, ActionReopenPullRequest, ActionAutoMergePullRequest: 667 if !permPR[i] { 668 continue 669 } 670 } 671 672 if err = db.Insert(ctx, act); err != nil { 673 return fmt.Errorf("insert new action: %w", err) 674 } 675 } 676 } 677 return nil 678 } 679 680 // NotifyWatchersActions creates batch of actions for every watcher. 681 func NotifyWatchersActions(ctx context.Context, acts []*Action) error { 682 ctx, committer, err := db.TxContext(ctx) 683 if err != nil { 684 return err 685 } 686 defer committer.Close() 687 for _, act := range acts { 688 if err := NotifyWatchers(ctx, act); err != nil { 689 return err 690 } 691 } 692 return committer.Commit() 693 } 694 695 // DeleteIssueActions delete all actions related with issueID 696 func DeleteIssueActions(ctx context.Context, repoID, issueID, issueIndex int64) error { 697 // delete actions assigned to this issue 698 e := db.GetEngine(ctx) 699 700 // MariaDB has a performance bug: https://jira.mariadb.org/browse/MDEV-16289 701 // so here it uses "DELETE ... WHERE IN" with pre-queried IDs. 702 var lastCommentID int64 703 commentIDs := make([]int64, 0, db.DefaultMaxInSize) 704 for { 705 commentIDs = commentIDs[:0] 706 err := e.Select("`id`").Table(&issues_model.Comment{}). 707 Where(builder.Eq{"issue_id": issueID}).And("`id` > ?", lastCommentID). 708 OrderBy("`id`").Limit(db.DefaultMaxInSize). 709 Find(&commentIDs) 710 if err != nil { 711 return err 712 } else if len(commentIDs) == 0 { 713 break 714 } else if _, err = db.GetEngine(ctx).In("comment_id", commentIDs).Delete(&Action{}); err != nil { 715 return err 716 } else { 717 lastCommentID = commentIDs[len(commentIDs)-1] 718 } 719 } 720 721 _, err := e.Where("repo_id = ?", repoID). 722 In("op_type", ActionCreateIssue, ActionCreatePullRequest). 723 Where("content LIKE ?", strconv.FormatInt(issueIndex, 10)+"|%"). // "IssueIndex|content..." 724 Delete(&Action{}) 725 return err 726 } 727 728 // CountActionCreatedUnixString count actions where created_unix is an empty string 729 func CountActionCreatedUnixString(ctx context.Context) (int64, error) { 730 if setting.Database.Type.IsSQLite3() { 731 return db.GetEngine(ctx).Where(`created_unix = ""`).Count(new(Action)) 732 } 733 return 0, nil 734 } 735 736 // FixActionCreatedUnixString set created_unix to zero if it is an empty string 737 func FixActionCreatedUnixString(ctx context.Context) (int64, error) { 738 if setting.Database.Type.IsSQLite3() { 739 res, err := db.GetEngine(ctx).Exec(`UPDATE action SET created_unix = 0 WHERE created_unix = ""`) 740 if err != nil { 741 return 0, err 742 } 743 return res.RowsAffected() 744 } 745 return 0, nil 746 }