code.gitea.io/gitea@v1.22.3/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 Issue *issues_model.Issue `xorm:"-"` // get the issue id from content 152 IsDeleted bool `xorm:"NOT NULL DEFAULT false"` 153 RefName string 154 IsPrivate bool `xorm:"NOT NULL DEFAULT false"` 155 Content string `xorm:"TEXT"` 156 CreatedUnix timeutil.TimeStamp `xorm:"created"` 157 } 158 159 func init() { 160 db.RegisterModel(new(Action)) 161 } 162 163 // TableIndices implements xorm's TableIndices interface 164 func (a *Action) TableIndices() []*schemas.Index { 165 repoIndex := schemas.NewIndex("r_u_d", schemas.IndexType) 166 repoIndex.AddColumn("repo_id", "user_id", "is_deleted") 167 168 actUserIndex := schemas.NewIndex("au_r_c_u_d", schemas.IndexType) 169 actUserIndex.AddColumn("act_user_id", "repo_id", "created_unix", "user_id", "is_deleted") 170 171 cudIndex := schemas.NewIndex("c_u_d", schemas.IndexType) 172 cudIndex.AddColumn("created_unix", "user_id", "is_deleted") 173 174 indices := []*schemas.Index{actUserIndex, repoIndex, cudIndex} 175 176 return indices 177 } 178 179 // GetOpType gets the ActionType of this action. 180 func (a *Action) GetOpType() ActionType { 181 return a.OpType 182 } 183 184 // LoadActUser loads a.ActUser 185 func (a *Action) LoadActUser(ctx context.Context) { 186 if a.ActUser != nil { 187 return 188 } 189 var err error 190 a.ActUser, err = user_model.GetUserByID(ctx, a.ActUserID) 191 if err == nil { 192 return 193 } else if user_model.IsErrUserNotExist(err) { 194 a.ActUser = user_model.NewGhostUser() 195 } else { 196 log.Error("GetUserByID(%d): %v", a.ActUserID, err) 197 } 198 } 199 200 func (a *Action) loadRepo(ctx context.Context) { 201 if a.Repo != nil { 202 return 203 } 204 var err error 205 a.Repo, err = repo_model.GetRepositoryByID(ctx, a.RepoID) 206 if err != nil { 207 log.Error("repo_model.GetRepositoryByID(%d): %v", a.RepoID, err) 208 } 209 } 210 211 // GetActFullName gets the action's user full name. 212 func (a *Action) GetActFullName(ctx context.Context) string { 213 a.LoadActUser(ctx) 214 return a.ActUser.FullName 215 } 216 217 // GetActUserName gets the action's user name. 218 func (a *Action) GetActUserName(ctx context.Context) string { 219 a.LoadActUser(ctx) 220 return a.ActUser.Name 221 } 222 223 // ShortActUserName gets the action's user name trimmed to max 20 224 // chars. 225 func (a *Action) ShortActUserName(ctx context.Context) string { 226 return base.EllipsisString(a.GetActUserName(ctx), 20) 227 } 228 229 // GetActDisplayName gets the action's display name based on DEFAULT_SHOW_FULL_NAME, or falls back to the username if it is blank. 230 func (a *Action) GetActDisplayName(ctx context.Context) string { 231 if setting.UI.DefaultShowFullName { 232 trimmedFullName := strings.TrimSpace(a.GetActFullName(ctx)) 233 if len(trimmedFullName) > 0 { 234 return trimmedFullName 235 } 236 } 237 return a.ShortActUserName(ctx) 238 } 239 240 // GetActDisplayNameTitle gets the action's display name used for the title (tooltip) based on DEFAULT_SHOW_FULL_NAME 241 func (a *Action) GetActDisplayNameTitle(ctx context.Context) string { 242 if setting.UI.DefaultShowFullName { 243 return a.ShortActUserName(ctx) 244 } 245 return a.GetActFullName(ctx) 246 } 247 248 // GetRepoUserName returns the name of the action repository owner. 249 func (a *Action) GetRepoUserName(ctx context.Context) string { 250 a.loadRepo(ctx) 251 return a.Repo.OwnerName 252 } 253 254 // ShortRepoUserName returns the name of the action repository owner 255 // trimmed to max 20 chars. 256 func (a *Action) ShortRepoUserName(ctx context.Context) string { 257 return base.EllipsisString(a.GetRepoUserName(ctx), 20) 258 } 259 260 // GetRepoName returns the name of the action repository. 261 func (a *Action) GetRepoName(ctx context.Context) string { 262 a.loadRepo(ctx) 263 return a.Repo.Name 264 } 265 266 // ShortRepoName returns the name of the action repository 267 // trimmed to max 33 chars. 268 func (a *Action) ShortRepoName(ctx context.Context) string { 269 return base.EllipsisString(a.GetRepoName(ctx), 33) 270 } 271 272 // GetRepoPath returns the virtual path to the action repository. 273 func (a *Action) GetRepoPath(ctx context.Context) string { 274 return path.Join(a.GetRepoUserName(ctx), a.GetRepoName(ctx)) 275 } 276 277 // ShortRepoPath returns the virtual path to the action repository 278 // trimmed to max 20 + 1 + 33 chars. 279 func (a *Action) ShortRepoPath(ctx context.Context) string { 280 return path.Join(a.ShortRepoUserName(ctx), a.ShortRepoName(ctx)) 281 } 282 283 // GetRepoLink returns relative link to action repository. 284 func (a *Action) GetRepoLink(ctx context.Context) string { 285 // path.Join will skip empty strings 286 return path.Join(setting.AppSubURL, "/", url.PathEscape(a.GetRepoUserName(ctx)), url.PathEscape(a.GetRepoName(ctx))) 287 } 288 289 // GetRepoAbsoluteLink returns the absolute link to action repository. 290 func (a *Action) GetRepoAbsoluteLink(ctx context.Context) string { 291 return setting.AppURL + url.PathEscape(a.GetRepoUserName(ctx)) + "/" + url.PathEscape(a.GetRepoName(ctx)) 292 } 293 294 func (a *Action) loadComment(ctx context.Context) (err error) { 295 if a.CommentID == 0 || a.Comment != nil { 296 return nil 297 } 298 a.Comment, err = issues_model.GetCommentByID(ctx, a.CommentID) 299 return err 300 } 301 302 // GetCommentHTMLURL returns link to action comment. 303 func (a *Action) GetCommentHTMLURL(ctx context.Context) string { 304 if a == nil { 305 return "#" 306 } 307 _ = a.loadComment(ctx) 308 if a.Comment != nil { 309 return a.Comment.HTMLURL(ctx) 310 } 311 312 if err := a.LoadIssue(ctx); err != nil || a.Issue == nil { 313 return "#" 314 } 315 if err := a.Issue.LoadRepo(ctx); err != nil { 316 return "#" 317 } 318 319 return a.Issue.HTMLURL() 320 } 321 322 // GetCommentLink returns link to action comment. 323 func (a *Action) GetCommentLink(ctx context.Context) string { 324 if a == nil { 325 return "#" 326 } 327 _ = a.loadComment(ctx) 328 if a.Comment != nil { 329 return a.Comment.Link(ctx) 330 } 331 332 if err := a.LoadIssue(ctx); err != nil || a.Issue == nil { 333 return "#" 334 } 335 if err := a.Issue.LoadRepo(ctx); err != nil { 336 return "#" 337 } 338 339 return a.Issue.Link() 340 } 341 342 // GetBranch returns the action's repository branch. 343 func (a *Action) GetBranch() string { 344 return strings.TrimPrefix(a.RefName, git.BranchPrefix) 345 } 346 347 // GetRefLink returns the action's ref link. 348 func (a *Action) GetRefLink(ctx context.Context) string { 349 return git.RefURL(a.GetRepoLink(ctx), a.RefName) 350 } 351 352 // GetTag returns the action's repository tag. 353 func (a *Action) GetTag() string { 354 return strings.TrimPrefix(a.RefName, git.TagPrefix) 355 } 356 357 // GetContent returns the action's content. 358 func (a *Action) GetContent() string { 359 return a.Content 360 } 361 362 // GetCreate returns the action creation time. 363 func (a *Action) GetCreate() time.Time { 364 return a.CreatedUnix.AsTime() 365 } 366 367 func (a *Action) IsIssueEvent() bool { 368 return a.OpType.InActions("comment_issue", "approve_pull_request", "reject_pull_request", "comment_pull", "merge_pull_request") 369 } 370 371 // GetIssueInfos returns a list of associated information with the action. 372 func (a *Action) GetIssueInfos() []string { 373 // make sure it always returns 3 elements, because there are some access to the a[1] and a[2] without checking the length 374 ret := strings.SplitN(a.Content, "|", 3) 375 for len(ret) < 3 { 376 ret = append(ret, "") 377 } 378 return ret 379 } 380 381 func (a *Action) getIssueIndex() int64 { 382 infos := a.GetIssueInfos() 383 if len(infos) == 0 { 384 return 0 385 } 386 index, _ := strconv.ParseInt(infos[0], 10, 64) 387 return index 388 } 389 390 func (a *Action) LoadIssue(ctx context.Context) error { 391 if a.Issue != nil { 392 return nil 393 } 394 if index := a.getIssueIndex(); index > 0 { 395 issue, err := issues_model.GetIssueByIndex(ctx, a.RepoID, index) 396 if err != nil { 397 return err 398 } 399 a.Issue = issue 400 a.Issue.Repo = a.Repo 401 } 402 return nil 403 } 404 405 // GetIssueTitle returns the title of first issue associated with the action. 406 func (a *Action) GetIssueTitle(ctx context.Context) string { 407 if err := a.LoadIssue(ctx); err != nil { 408 log.Error("LoadIssue: %v", err) 409 return "<500 when get issue>" 410 } 411 if a.Issue == nil { 412 return "<Issue not found>" 413 } 414 return a.Issue.Title 415 } 416 417 // GetIssueContent returns the content of first issue associated with this action. 418 func (a *Action) GetIssueContent(ctx context.Context) string { 419 if err := a.LoadIssue(ctx); err != nil { 420 log.Error("LoadIssue: %v", err) 421 return "<500 when get issue>" 422 } 423 if a.Issue == nil { 424 return "<Content not found>" 425 } 426 return a.Issue.Content 427 } 428 429 // GetFeedsOptions options for retrieving feeds 430 type GetFeedsOptions struct { 431 db.ListOptions 432 RequestedUser *user_model.User // the user we want activity for 433 RequestedTeam *organization.Team // the team we want activity for 434 RequestedRepo *repo_model.Repository // the repo we want activity for 435 Actor *user_model.User // the user viewing the activity 436 IncludePrivate bool // include private actions 437 OnlyPerformedBy bool // only actions performed by requested user 438 IncludeDeleted bool // include deleted actions 439 Date string // the day we want activity for: YYYY-MM-DD 440 } 441 442 // GetFeeds returns actions according to the provided options 443 func GetFeeds(ctx context.Context, opts GetFeedsOptions) (ActionList, int64, error) { 444 if opts.RequestedUser == nil && opts.RequestedTeam == nil && opts.RequestedRepo == nil { 445 return nil, 0, fmt.Errorf("need at least one of these filters: RequestedUser, RequestedTeam, RequestedRepo") 446 } 447 448 cond, err := activityQueryCondition(ctx, opts) 449 if err != nil { 450 return nil, 0, err 451 } 452 453 sess := db.GetEngine(ctx).Where(cond) 454 455 opts.SetDefaultValues() 456 sess = db.SetSessionPagination(sess, &opts) 457 458 actions := make([]*Action, 0, opts.PageSize) 459 count, err := sess.Desc("`action`.created_unix").FindAndCount(&actions) 460 if err != nil { 461 return nil, 0, fmt.Errorf("FindAndCount: %w", err) 462 } 463 464 if err := ActionList(actions).LoadAttributes(ctx); err != nil { 465 return nil, 0, fmt.Errorf("LoadAttributes: %w", err) 466 } 467 468 return actions, count, nil 469 } 470 471 // ActivityReadable return whether doer can read activities of user 472 func ActivityReadable(user, doer *user_model.User) bool { 473 return !user.KeepActivityPrivate || 474 doer != nil && (doer.IsAdmin || user.ID == doer.ID) 475 } 476 477 func activityQueryCondition(ctx context.Context, opts GetFeedsOptions) (builder.Cond, error) { 478 cond := builder.NewCond() 479 480 if opts.RequestedTeam != nil && opts.RequestedUser == nil { 481 org, err := user_model.GetUserByID(ctx, opts.RequestedTeam.OrgID) 482 if err != nil { 483 return nil, err 484 } 485 opts.RequestedUser = org 486 } 487 488 // check activity visibility for actor ( similar to activityReadable() ) 489 if opts.Actor == nil { 490 cond = cond.And(builder.In("act_user_id", 491 builder.Select("`user`.id").Where( 492 builder.Eq{"keep_activity_private": false, "visibility": structs.VisibleTypePublic}, 493 ).From("`user`"), 494 )) 495 } else if !opts.Actor.IsAdmin { 496 uidCond := builder.Select("`user`.id").From("`user`").Where( 497 builder.Eq{"keep_activity_private": false}. 498 And(builder.In("visibility", structs.VisibleTypePublic, structs.VisibleTypeLimited))). 499 Or(builder.Eq{"id": opts.Actor.ID}) 500 501 if opts.RequestedUser != nil { 502 if opts.RequestedUser.IsOrganization() { 503 // An organization can always see the activities whose `act_user_id` is the same as its id. 504 uidCond = uidCond.Or(builder.Eq{"id": opts.RequestedUser.ID}) 505 } else { 506 // A user can always see the activities of the organizations to which the user belongs. 507 uidCond = uidCond.Or( 508 builder.Eq{"type": user_model.UserTypeOrganization}. 509 And(builder.In("`user`.id", builder.Select("org_id"). 510 Where(builder.Eq{"uid": opts.RequestedUser.ID}). 511 From("team_user"))), 512 ) 513 } 514 } 515 516 cond = cond.And(builder.In("act_user_id", uidCond)) 517 } 518 519 // check readable repositories by doer/actor 520 if opts.Actor == nil || !opts.Actor.IsAdmin { 521 cond = cond.And(builder.In("repo_id", repo_model.AccessibleRepoIDsQuery(opts.Actor))) 522 } 523 524 if opts.RequestedRepo != nil { 525 // repo's actions could have duplicate items, see the comment of NotifyWatchers 526 // so here we only filter the "original items", aka: user_id == act_user_id 527 cond = cond.And( 528 builder.Eq{"`action`.repo_id": opts.RequestedRepo.ID}, 529 builder.Expr("`action`.user_id = `action`.act_user_id"), 530 ) 531 } 532 533 if opts.RequestedTeam != nil { 534 env := organization.OrgFromUser(opts.RequestedUser).AccessibleTeamReposEnv(ctx, opts.RequestedTeam) 535 teamRepoIDs, err := env.RepoIDs(1, opts.RequestedUser.NumRepos) 536 if err != nil { 537 return nil, fmt.Errorf("GetTeamRepositories: %w", err) 538 } 539 cond = cond.And(builder.In("repo_id", teamRepoIDs)) 540 } 541 542 if opts.RequestedUser != nil { 543 cond = cond.And(builder.Eq{"user_id": opts.RequestedUser.ID}) 544 545 if opts.OnlyPerformedBy { 546 cond = cond.And(builder.Eq{"act_user_id": opts.RequestedUser.ID}) 547 } 548 } 549 550 if !opts.IncludePrivate { 551 cond = cond.And(builder.Eq{"`action`.is_private": false}) 552 } 553 if !opts.IncludeDeleted { 554 cond = cond.And(builder.Eq{"is_deleted": false}) 555 } 556 557 if opts.Date != "" { 558 dateLow, err := time.ParseInLocation("2006-01-02", opts.Date, setting.DefaultUILocation) 559 if err != nil { 560 log.Warn("Unable to parse %s, filter not applied: %v", opts.Date, err) 561 } else { 562 dateHigh := dateLow.Add(86399000000000) // 23h59m59s 563 564 cond = cond.And(builder.Gte{"`action`.created_unix": dateLow.Unix()}) 565 cond = cond.And(builder.Lte{"`action`.created_unix": dateHigh.Unix()}) 566 } 567 } 568 569 return cond, nil 570 } 571 572 // DeleteOldActions deletes all old actions from database. 573 func DeleteOldActions(ctx context.Context, olderThan time.Duration) (err error) { 574 if olderThan <= 0 { 575 return nil 576 } 577 578 _, err = db.GetEngine(ctx).Where("created_unix < ?", time.Now().Add(-olderThan).Unix()).Delete(&Action{}) 579 return err 580 } 581 582 // NotifyWatchers creates batch of actions for every watcher. 583 // It could insert duplicate actions for a repository action, like this: 584 // * Original action: UserID=1 (the real actor), ActUserID=1 585 // * Organization action: UserID=100 (the repo's org), ActUserID=1 586 // * Watcher action: UserID=20 (a user who is watching a repo), ActUserID=1 587 func NotifyWatchers(ctx context.Context, actions ...*Action) error { 588 var watchers []*repo_model.Watch 589 var repo *repo_model.Repository 590 var err error 591 var permCode []bool 592 var permIssue []bool 593 var permPR []bool 594 595 e := db.GetEngine(ctx) 596 597 for _, act := range actions { 598 repoChanged := repo == nil || repo.ID != act.RepoID 599 600 if repoChanged { 601 // Add feeds for user self and all watchers. 602 watchers, err = repo_model.GetWatchers(ctx, act.RepoID) 603 if err != nil { 604 return fmt.Errorf("get watchers: %w", err) 605 } 606 } 607 608 // Add feed for actioner. 609 act.UserID = act.ActUserID 610 if _, err = e.Insert(act); err != nil { 611 return fmt.Errorf("insert new actioner: %w", err) 612 } 613 614 if repoChanged { 615 act.loadRepo(ctx) 616 repo = act.Repo 617 618 // check repo owner exist. 619 if err := act.Repo.LoadOwner(ctx); err != nil { 620 return fmt.Errorf("can't get repo owner: %w", err) 621 } 622 } else if act.Repo == nil { 623 act.Repo = repo 624 } 625 626 // Add feed for organization 627 if act.Repo.Owner.IsOrganization() && act.ActUserID != act.Repo.Owner.ID { 628 act.ID = 0 629 act.UserID = act.Repo.Owner.ID 630 if err = db.Insert(ctx, act); err != nil { 631 return fmt.Errorf("insert new actioner: %w", err) 632 } 633 } 634 635 if repoChanged { 636 permCode = make([]bool, len(watchers)) 637 permIssue = make([]bool, len(watchers)) 638 permPR = make([]bool, len(watchers)) 639 for i, watcher := range watchers { 640 user, err := user_model.GetUserByID(ctx, watcher.UserID) 641 if err != nil { 642 permCode[i] = false 643 permIssue[i] = false 644 permPR[i] = false 645 continue 646 } 647 perm, err := access_model.GetUserRepoPermission(ctx, repo, user) 648 if err != nil { 649 permCode[i] = false 650 permIssue[i] = false 651 permPR[i] = false 652 continue 653 } 654 permCode[i] = perm.CanRead(unit.TypeCode) 655 permIssue[i] = perm.CanRead(unit.TypeIssues) 656 permPR[i] = perm.CanRead(unit.TypePullRequests) 657 } 658 } 659 660 for i, watcher := range watchers { 661 if act.ActUserID == watcher.UserID { 662 continue 663 } 664 act.ID = 0 665 act.UserID = watcher.UserID 666 act.Repo.Units = nil 667 668 switch act.OpType { 669 case ActionCommitRepo, ActionPushTag, ActionDeleteTag, ActionPublishRelease, ActionDeleteBranch: 670 if !permCode[i] { 671 continue 672 } 673 case ActionCreateIssue, ActionCommentIssue, ActionCloseIssue, ActionReopenIssue: 674 if !permIssue[i] { 675 continue 676 } 677 case ActionCreatePullRequest, ActionCommentPull, ActionMergePullRequest, ActionClosePullRequest, ActionReopenPullRequest, ActionAutoMergePullRequest: 678 if !permPR[i] { 679 continue 680 } 681 } 682 683 if err = db.Insert(ctx, act); err != nil { 684 return fmt.Errorf("insert new action: %w", err) 685 } 686 } 687 } 688 return nil 689 } 690 691 // NotifyWatchersActions creates batch of actions for every watcher. 692 func NotifyWatchersActions(ctx context.Context, acts []*Action) error { 693 ctx, committer, err := db.TxContext(ctx) 694 if err != nil { 695 return err 696 } 697 defer committer.Close() 698 for _, act := range acts { 699 if err := NotifyWatchers(ctx, act); err != nil { 700 return err 701 } 702 } 703 return committer.Commit() 704 } 705 706 // DeleteIssueActions delete all actions related with issueID 707 func DeleteIssueActions(ctx context.Context, repoID, issueID, issueIndex int64) error { 708 // delete actions assigned to this issue 709 e := db.GetEngine(ctx) 710 711 // MariaDB has a performance bug: https://jira.mariadb.org/browse/MDEV-16289 712 // so here it uses "DELETE ... WHERE IN" with pre-queried IDs. 713 var lastCommentID int64 714 commentIDs := make([]int64, 0, db.DefaultMaxInSize) 715 for { 716 commentIDs = commentIDs[:0] 717 err := e.Select("`id`").Table(&issues_model.Comment{}). 718 Where(builder.Eq{"issue_id": issueID}).And("`id` > ?", lastCommentID). 719 OrderBy("`id`").Limit(db.DefaultMaxInSize). 720 Find(&commentIDs) 721 if err != nil { 722 return err 723 } else if len(commentIDs) == 0 { 724 break 725 } else if _, err = db.GetEngine(ctx).In("comment_id", commentIDs).Delete(&Action{}); err != nil { 726 return err 727 } 728 lastCommentID = commentIDs[len(commentIDs)-1] 729 } 730 731 _, err := e.Where("repo_id = ?", repoID). 732 In("op_type", ActionCreateIssue, ActionCreatePullRequest). 733 Where("content LIKE ?", strconv.FormatInt(issueIndex, 10)+"|%"). // "IssueIndex|content..." 734 Delete(&Action{}) 735 return err 736 } 737 738 // CountActionCreatedUnixString count actions where created_unix is an empty string 739 func CountActionCreatedUnixString(ctx context.Context) (int64, error) { 740 if setting.Database.Type.IsSQLite3() { 741 return db.GetEngine(ctx).Where(`created_unix = ""`).Count(new(Action)) 742 } 743 return 0, nil 744 } 745 746 // FixActionCreatedUnixString set created_unix to zero if it is an empty string 747 func FixActionCreatedUnixString(ctx context.Context) (int64, error) { 748 if setting.Database.Type.IsSQLite3() { 749 res, err := db.GetEngine(ctx).Exec(`UPDATE action SET created_unix = 0 WHERE created_unix = ""`) 750 if err != nil { 751 return 0, err 752 } 753 return res.RowsAffected() 754 } 755 return 0, nil 756 }