first-commit
This commit is contained in:
162
services/feed/feed.go
Normal file
162
services/feed/feed.go
Normal file
@@ -0,0 +1,162 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package feed
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
activities_model "code.gitea.io/gitea/models/activities"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
access_model "code.gitea.io/gitea/models/perm/access"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
func GetFeedsForDashboard(ctx context.Context, opts activities_model.GetFeedsOptions) (activities_model.ActionList, int, error) {
|
||||
opts.DontCount = opts.RequestedTeam == nil && opts.Date == ""
|
||||
results, cnt, err := activities_model.GetFeeds(ctx, opts)
|
||||
return results, util.Iif(opts.DontCount, -1, int(cnt)), err
|
||||
}
|
||||
|
||||
// GetFeeds returns actions according to the provided options
|
||||
func GetFeeds(ctx context.Context, opts activities_model.GetFeedsOptions) (activities_model.ActionList, int64, error) {
|
||||
return activities_model.GetFeeds(ctx, opts)
|
||||
}
|
||||
|
||||
// notifyWatchers creates batch of actions for every watcher.
|
||||
// It could insert duplicate actions for a repository action, like this:
|
||||
// * Original action: UserID=1 (the real actor), ActUserID=1
|
||||
// * Organization action: UserID=100 (the repo's org), ActUserID=1
|
||||
// * Watcher action: UserID=20 (a user who is watching a repo), ActUserID=1
|
||||
func notifyWatchers(ctx context.Context, act *activities_model.Action, watchers []*repo_model.Watch, permCode, permIssue, permPR []bool) error {
|
||||
// MySQL has TEXT length limit 65535.
|
||||
// Sometimes the content is "field1|field2|field3", sometimes the content is JSON (ActionMirrorSyncPush, ActionCommitRepo, ActionPushTag, etc...)
|
||||
if left, right := util.EllipsisDisplayStringX(act.Content, 65535); right != "" {
|
||||
if strings.HasPrefix(act.Content, `{"`) && strings.HasSuffix(act.Content, `}`) {
|
||||
// FIXME: at the moment we can do nothing if the content is JSON and it is too long
|
||||
act.Content = "{}"
|
||||
} else {
|
||||
act.Content = left
|
||||
}
|
||||
}
|
||||
|
||||
// Add feed for actor.
|
||||
act.UserID = act.ActUserID
|
||||
if err := db.Insert(ctx, act); err != nil {
|
||||
return fmt.Errorf("insert new actioner: %w", err)
|
||||
}
|
||||
|
||||
// Add feed for organization
|
||||
if act.Repo.Owner.IsOrganization() && act.ActUserID != act.Repo.Owner.ID {
|
||||
act.ID = 0
|
||||
act.UserID = act.Repo.Owner.ID
|
||||
if err := db.Insert(ctx, act); err != nil {
|
||||
return fmt.Errorf("insert new actioner: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
for i, watcher := range watchers {
|
||||
if act.ActUserID == watcher.UserID {
|
||||
continue
|
||||
}
|
||||
act.ID = 0
|
||||
act.UserID = watcher.UserID
|
||||
act.Repo.Units = nil
|
||||
|
||||
switch act.OpType {
|
||||
case activities_model.ActionCommitRepo, activities_model.ActionPushTag, activities_model.ActionDeleteTag, activities_model.ActionPublishRelease, activities_model.ActionDeleteBranch:
|
||||
if !permCode[i] {
|
||||
continue
|
||||
}
|
||||
case activities_model.ActionCreateIssue, activities_model.ActionCommentIssue, activities_model.ActionCloseIssue, activities_model.ActionReopenIssue:
|
||||
if !permIssue[i] {
|
||||
continue
|
||||
}
|
||||
case activities_model.ActionCreatePullRequest, activities_model.ActionCommentPull, activities_model.ActionMergePullRequest, activities_model.ActionClosePullRequest, activities_model.ActionReopenPullRequest, activities_model.ActionAutoMergePullRequest:
|
||||
if !permPR[i] {
|
||||
continue
|
||||
}
|
||||
default:
|
||||
}
|
||||
|
||||
if err := db.Insert(ctx, act); err != nil {
|
||||
return fmt.Errorf("insert new action: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// NotifyWatchers creates batch of actions for every watcher.
|
||||
func NotifyWatchers(ctx context.Context, acts ...*activities_model.Action) error {
|
||||
return db.WithTx(ctx, func(ctx context.Context) error {
|
||||
if len(acts) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
repoID := acts[0].RepoID
|
||||
if repoID == 0 {
|
||||
setting.PanicInDevOrTesting("action should belong to a repo")
|
||||
return nil
|
||||
}
|
||||
if err := acts[0].LoadRepo(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
repo := acts[0].Repo
|
||||
if err := repo.LoadOwner(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
actUserID := acts[0].ActUserID
|
||||
|
||||
// Add feeds for user self and all watchers.
|
||||
watchers, err := repo_model.GetWatchers(ctx, repoID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get watchers: %w", err)
|
||||
}
|
||||
|
||||
permCode := make([]bool, len(watchers))
|
||||
permIssue := make([]bool, len(watchers))
|
||||
permPR := make([]bool, len(watchers))
|
||||
for i, watcher := range watchers {
|
||||
user, err := user_model.GetUserByID(ctx, watcher.UserID)
|
||||
if err != nil {
|
||||
permCode[i] = false
|
||||
permIssue[i] = false
|
||||
permPR[i] = false
|
||||
continue
|
||||
}
|
||||
perm, err := access_model.GetUserRepoPermission(ctx, repo, user)
|
||||
if err != nil {
|
||||
permCode[i] = false
|
||||
permIssue[i] = false
|
||||
permPR[i] = false
|
||||
continue
|
||||
}
|
||||
permCode[i] = perm.CanRead(unit.TypeCode)
|
||||
permIssue[i] = perm.CanRead(unit.TypeIssues)
|
||||
permPR[i] = perm.CanRead(unit.TypePullRequests)
|
||||
}
|
||||
|
||||
for _, act := range acts {
|
||||
if act.RepoID != repoID {
|
||||
setting.PanicInDevOrTesting("action should belong to the same repo, expected[%d], got[%d] ", repoID, act.RepoID)
|
||||
}
|
||||
if act.ActUserID != actUserID {
|
||||
setting.PanicInDevOrTesting("action should have the same actor, expected[%d], got[%d] ", actUserID, act.ActUserID)
|
||||
}
|
||||
|
||||
act.Repo = repo
|
||||
if err := notifyWatchers(ctx, act, watchers, permCode, permIssue, permPR); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
202
services/feed/feed_test.go
Normal file
202
services/feed/feed_test.go
Normal file
@@ -0,0 +1,202 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package feed
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
activities_model "code.gitea.io/gitea/models/activities"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetFeeds(t *testing.T) {
|
||||
// test with an individual user
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
|
||||
actions, count, err := GetFeeds(db.DefaultContext, activities_model.GetFeedsOptions{
|
||||
RequestedUser: user,
|
||||
Actor: user,
|
||||
IncludePrivate: true,
|
||||
OnlyPerformedBy: false,
|
||||
IncludeDeleted: true,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
if assert.Len(t, actions, 1) {
|
||||
assert.EqualValues(t, 1, actions[0].ID)
|
||||
assert.Equal(t, user.ID, actions[0].UserID)
|
||||
}
|
||||
assert.Equal(t, int64(1), count)
|
||||
|
||||
actions, count, err = GetFeeds(db.DefaultContext, activities_model.GetFeedsOptions{
|
||||
RequestedUser: user,
|
||||
Actor: user,
|
||||
IncludePrivate: false,
|
||||
OnlyPerformedBy: false,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, actions)
|
||||
assert.Equal(t, int64(0), count)
|
||||
}
|
||||
|
||||
func TestGetFeedsForRepos(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
privRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
|
||||
pubRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 8})
|
||||
|
||||
// private repo & no login
|
||||
actions, count, err := GetFeeds(db.DefaultContext, activities_model.GetFeedsOptions{
|
||||
RequestedRepo: privRepo,
|
||||
IncludePrivate: true,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, actions)
|
||||
assert.Equal(t, int64(0), count)
|
||||
|
||||
// public repo & no login
|
||||
actions, count, err = GetFeeds(db.DefaultContext, activities_model.GetFeedsOptions{
|
||||
RequestedRepo: pubRepo,
|
||||
IncludePrivate: true,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, actions, 1)
|
||||
assert.Equal(t, int64(1), count)
|
||||
|
||||
// private repo and login
|
||||
actions, count, err = GetFeeds(db.DefaultContext, activities_model.GetFeedsOptions{
|
||||
RequestedRepo: privRepo,
|
||||
IncludePrivate: true,
|
||||
Actor: user,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, actions, 1)
|
||||
assert.Equal(t, int64(1), count)
|
||||
|
||||
// public repo & login
|
||||
actions, count, err = GetFeeds(db.DefaultContext, activities_model.GetFeedsOptions{
|
||||
RequestedRepo: pubRepo,
|
||||
IncludePrivate: true,
|
||||
Actor: user,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, actions, 1)
|
||||
assert.Equal(t, int64(1), count)
|
||||
}
|
||||
|
||||
func TestGetFeeds2(t *testing.T) {
|
||||
// test with an organization user
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
org := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3})
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
|
||||
actions, count, err := GetFeeds(db.DefaultContext, activities_model.GetFeedsOptions{
|
||||
RequestedUser: org,
|
||||
Actor: user,
|
||||
IncludePrivate: true,
|
||||
OnlyPerformedBy: false,
|
||||
IncludeDeleted: true,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, actions, 1)
|
||||
if assert.Len(t, actions, 1) {
|
||||
assert.EqualValues(t, 2, actions[0].ID)
|
||||
assert.Equal(t, org.ID, actions[0].UserID)
|
||||
}
|
||||
assert.Equal(t, int64(1), count)
|
||||
|
||||
actions, count, err = GetFeeds(db.DefaultContext, activities_model.GetFeedsOptions{
|
||||
RequestedUser: org,
|
||||
Actor: user,
|
||||
IncludePrivate: false,
|
||||
OnlyPerformedBy: false,
|
||||
IncludeDeleted: true,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, actions)
|
||||
assert.Equal(t, int64(0), count)
|
||||
}
|
||||
|
||||
func TestGetFeedsCorrupted(t *testing.T) {
|
||||
// Now we will not check for corrupted data in the feeds
|
||||
// users should run doctor to fix their data
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
|
||||
unittest.AssertExistsAndLoadBean(t, &activities_model.Action{
|
||||
ID: 8,
|
||||
RepoID: 1700,
|
||||
})
|
||||
|
||||
actions, count, err := GetFeeds(db.DefaultContext, activities_model.GetFeedsOptions{
|
||||
RequestedUser: user,
|
||||
Actor: user,
|
||||
IncludePrivate: true,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, actions, 1)
|
||||
assert.Equal(t, int64(1), count)
|
||||
}
|
||||
|
||||
func TestRepoActions(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
_ = db.TruncateBeans(db.DefaultContext, &activities_model.Action{})
|
||||
for i := range 3 {
|
||||
_ = db.Insert(db.DefaultContext, &activities_model.Action{
|
||||
UserID: 2 + int64(i),
|
||||
ActUserID: 2,
|
||||
RepoID: repo.ID,
|
||||
OpType: activities_model.ActionCommentIssue,
|
||||
})
|
||||
}
|
||||
count, _ := db.Count[activities_model.Action](db.DefaultContext, &db.ListOptions{})
|
||||
assert.EqualValues(t, 3, count)
|
||||
actions, _, err := GetFeeds(db.DefaultContext, activities_model.GetFeedsOptions{
|
||||
RequestedRepo: repo,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, actions, 1)
|
||||
}
|
||||
|
||||
func TestNotifyWatchers(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
action := &activities_model.Action{
|
||||
ActUserID: 8,
|
||||
RepoID: 1,
|
||||
OpType: activities_model.ActionStarRepo,
|
||||
}
|
||||
assert.NoError(t, NotifyWatchers(db.DefaultContext, action))
|
||||
|
||||
// One watchers are inactive, thus action is only created for user 8, 1, 4, 11
|
||||
unittest.AssertExistsAndLoadBean(t, &activities_model.Action{
|
||||
ActUserID: action.ActUserID,
|
||||
UserID: 8,
|
||||
RepoID: action.RepoID,
|
||||
OpType: action.OpType,
|
||||
})
|
||||
unittest.AssertExistsAndLoadBean(t, &activities_model.Action{
|
||||
ActUserID: action.ActUserID,
|
||||
UserID: 1,
|
||||
RepoID: action.RepoID,
|
||||
OpType: action.OpType,
|
||||
})
|
||||
unittest.AssertExistsAndLoadBean(t, &activities_model.Action{
|
||||
ActUserID: action.ActUserID,
|
||||
UserID: 4,
|
||||
RepoID: action.RepoID,
|
||||
OpType: action.OpType,
|
||||
})
|
||||
unittest.AssertExistsAndLoadBean(t, &activities_model.Action{
|
||||
ActUserID: action.ActUserID,
|
||||
UserID: 11,
|
||||
RepoID: action.RepoID,
|
||||
OpType: action.OpType,
|
||||
})
|
||||
}
|
476
services/feed/notifier.go
Normal file
476
services/feed/notifier.go
Normal file
@@ -0,0 +1,476 @@
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package feed
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
activities_model "code.gitea.io/gitea/models/activities"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/repository"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
notify_service "code.gitea.io/gitea/services/notify"
|
||||
)
|
||||
|
||||
type actionNotifier struct {
|
||||
notify_service.NullNotifier
|
||||
}
|
||||
|
||||
var _ notify_service.Notifier = &actionNotifier{}
|
||||
|
||||
func Init() error {
|
||||
notify_service.RegisterNotifier(NewNotifier())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewNotifier create a new actionNotifier notifier
|
||||
func NewNotifier() notify_service.Notifier {
|
||||
return &actionNotifier{}
|
||||
}
|
||||
|
||||
func (a *actionNotifier) NewIssue(ctx context.Context, issue *issues_model.Issue, mentions []*user_model.User) {
|
||||
if err := issue.LoadPoster(ctx); err != nil {
|
||||
log.Error("issue.LoadPoster: %v", err)
|
||||
return
|
||||
}
|
||||
if err := issue.LoadRepo(ctx); err != nil {
|
||||
log.Error("issue.LoadRepo: %v", err)
|
||||
return
|
||||
}
|
||||
repo := issue.Repo
|
||||
|
||||
if err := NotifyWatchers(ctx, &activities_model.Action{
|
||||
ActUserID: issue.Poster.ID,
|
||||
ActUser: issue.Poster,
|
||||
OpType: activities_model.ActionCreateIssue,
|
||||
Content: fmt.Sprintf("%d|%s", issue.Index, issue.Title),
|
||||
RepoID: repo.ID,
|
||||
Repo: repo,
|
||||
IsPrivate: repo.IsPrivate,
|
||||
}); err != nil {
|
||||
log.Error("NotifyWatchers: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// IssueChangeStatus notifies close or reopen issue to notifiers
|
||||
func (a *actionNotifier) IssueChangeStatus(ctx context.Context, doer *user_model.User, commitID string, issue *issues_model.Issue, actionComment *issues_model.Comment, closeOrReopen bool) {
|
||||
// Compose comment action, could be plain comment, close or reopen issue/pull request.
|
||||
// This object will be used to notify watchers in the end of function.
|
||||
act := &activities_model.Action{
|
||||
ActUserID: doer.ID,
|
||||
ActUser: doer,
|
||||
Content: fmt.Sprintf("%d|%s", issue.Index, ""),
|
||||
RepoID: issue.Repo.ID,
|
||||
Repo: issue.Repo,
|
||||
Comment: actionComment,
|
||||
CommentID: actionComment.ID,
|
||||
IsPrivate: issue.Repo.IsPrivate,
|
||||
}
|
||||
// Check comment type.
|
||||
if closeOrReopen {
|
||||
act.OpType = activities_model.ActionCloseIssue
|
||||
if issue.IsPull {
|
||||
act.OpType = activities_model.ActionClosePullRequest
|
||||
}
|
||||
} else {
|
||||
act.OpType = activities_model.ActionReopenIssue
|
||||
if issue.IsPull {
|
||||
act.OpType = activities_model.ActionReopenPullRequest
|
||||
}
|
||||
}
|
||||
|
||||
// Notify watchers for whatever action comes in, ignore if no action type.
|
||||
if err := NotifyWatchers(ctx, act); err != nil {
|
||||
log.Error("NotifyWatchers: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// CreateIssueComment notifies comment on an issue to notifiers
|
||||
func (a *actionNotifier) CreateIssueComment(ctx context.Context, doer *user_model.User, repo *repo_model.Repository,
|
||||
issue *issues_model.Issue, comment *issues_model.Comment, mentions []*user_model.User,
|
||||
) {
|
||||
act := &activities_model.Action{
|
||||
ActUserID: doer.ID,
|
||||
ActUser: doer,
|
||||
RepoID: issue.Repo.ID,
|
||||
Repo: issue.Repo,
|
||||
Comment: comment,
|
||||
CommentID: comment.ID,
|
||||
IsPrivate: issue.Repo.IsPrivate,
|
||||
}
|
||||
|
||||
truncatedContent, truncatedRight := util.EllipsisDisplayStringX(comment.Content, 200)
|
||||
if truncatedRight != "" {
|
||||
// in case the content is in a Latin family language, we remove the last broken word.
|
||||
lastSpaceIdx := strings.LastIndex(truncatedContent, " ")
|
||||
if lastSpaceIdx != -1 && (len(truncatedContent)-lastSpaceIdx < 15) {
|
||||
truncatedContent = truncatedContent[:lastSpaceIdx] + "…"
|
||||
}
|
||||
}
|
||||
act.Content = fmt.Sprintf("%d|%s", issue.Index, truncatedContent)
|
||||
|
||||
if issue.IsPull {
|
||||
act.OpType = activities_model.ActionCommentPull
|
||||
} else {
|
||||
act.OpType = activities_model.ActionCommentIssue
|
||||
}
|
||||
|
||||
// Notify watchers for whatever action comes in, ignore if no action type.
|
||||
if err := NotifyWatchers(ctx, act); err != nil {
|
||||
log.Error("NotifyWatchers: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *actionNotifier) NewPullRequest(ctx context.Context, pull *issues_model.PullRequest, mentions []*user_model.User) {
|
||||
if err := pull.LoadIssue(ctx); err != nil {
|
||||
log.Error("pull.LoadIssue: %v", err)
|
||||
return
|
||||
}
|
||||
if err := pull.Issue.LoadRepo(ctx); err != nil {
|
||||
log.Error("pull.Issue.LoadRepo: %v", err)
|
||||
return
|
||||
}
|
||||
if err := pull.Issue.LoadPoster(ctx); err != nil {
|
||||
log.Error("pull.Issue.LoadPoster: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := NotifyWatchers(ctx, &activities_model.Action{
|
||||
ActUserID: pull.Issue.Poster.ID,
|
||||
ActUser: pull.Issue.Poster,
|
||||
OpType: activities_model.ActionCreatePullRequest,
|
||||
Content: fmt.Sprintf("%d|%s", pull.Issue.Index, pull.Issue.Title),
|
||||
RepoID: pull.Issue.Repo.ID,
|
||||
Repo: pull.Issue.Repo,
|
||||
IsPrivate: pull.Issue.Repo.IsPrivate,
|
||||
}); err != nil {
|
||||
log.Error("NotifyWatchers: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *actionNotifier) RenameRepository(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, oldRepoName string) {
|
||||
if err := NotifyWatchers(ctx, &activities_model.Action{
|
||||
ActUserID: doer.ID,
|
||||
ActUser: doer,
|
||||
OpType: activities_model.ActionRenameRepo,
|
||||
RepoID: repo.ID,
|
||||
Repo: repo,
|
||||
IsPrivate: repo.IsPrivate,
|
||||
Content: oldRepoName,
|
||||
}); err != nil {
|
||||
log.Error("NotifyWatchers: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *actionNotifier) TransferRepository(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, oldOwnerName string) {
|
||||
if err := NotifyWatchers(ctx, &activities_model.Action{
|
||||
ActUserID: doer.ID,
|
||||
ActUser: doer,
|
||||
OpType: activities_model.ActionTransferRepo,
|
||||
RepoID: repo.ID,
|
||||
Repo: repo,
|
||||
IsPrivate: repo.IsPrivate,
|
||||
Content: path.Join(oldOwnerName, repo.Name),
|
||||
}); err != nil {
|
||||
log.Error("NotifyWatchers: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *actionNotifier) CreateRepository(ctx context.Context, doer, u *user_model.User, repo *repo_model.Repository) {
|
||||
if err := NotifyWatchers(ctx, &activities_model.Action{
|
||||
ActUserID: doer.ID,
|
||||
ActUser: doer,
|
||||
OpType: activities_model.ActionCreateRepo,
|
||||
RepoID: repo.ID,
|
||||
Repo: repo,
|
||||
IsPrivate: repo.IsPrivate,
|
||||
}); err != nil {
|
||||
log.Error("notify watchers '%d/%d': %v", doer.ID, repo.ID, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *actionNotifier) ForkRepository(ctx context.Context, doer *user_model.User, oldRepo, repo *repo_model.Repository) {
|
||||
if err := NotifyWatchers(ctx, &activities_model.Action{
|
||||
ActUserID: doer.ID,
|
||||
ActUser: doer,
|
||||
OpType: activities_model.ActionCreateRepo,
|
||||
RepoID: repo.ID,
|
||||
Repo: repo,
|
||||
IsPrivate: repo.IsPrivate,
|
||||
}); err != nil {
|
||||
log.Error("notify watchers '%d/%d': %v", doer.ID, repo.ID, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *actionNotifier) PullRequestReview(ctx context.Context, pr *issues_model.PullRequest, review *issues_model.Review, comment *issues_model.Comment, mentions []*user_model.User) {
|
||||
if err := review.LoadReviewer(ctx); err != nil {
|
||||
log.Error("LoadReviewer '%d/%d': %v", review.ID, review.ReviewerID, err)
|
||||
return
|
||||
}
|
||||
if err := review.LoadCodeComments(ctx); err != nil {
|
||||
log.Error("LoadCodeComments '%d/%d': %v", review.Reviewer.ID, review.ID, err)
|
||||
return
|
||||
}
|
||||
|
||||
actions := make([]*activities_model.Action, 0, 10)
|
||||
for _, lines := range review.CodeComments {
|
||||
for _, comments := range lines {
|
||||
for _, comm := range comments {
|
||||
actions = append(actions, &activities_model.Action{
|
||||
ActUserID: review.Reviewer.ID,
|
||||
ActUser: review.Reviewer,
|
||||
Content: fmt.Sprintf("%d|%s", review.Issue.Index, strings.Split(comm.Content, "\n")[0]),
|
||||
OpType: activities_model.ActionCommentPull,
|
||||
RepoID: review.Issue.RepoID,
|
||||
Repo: review.Issue.Repo,
|
||||
IsPrivate: review.Issue.Repo.IsPrivate,
|
||||
Comment: comm,
|
||||
CommentID: comm.ID,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if review.Type != issues_model.ReviewTypeComment || strings.TrimSpace(comment.Content) != "" {
|
||||
action := &activities_model.Action{
|
||||
ActUserID: review.Reviewer.ID,
|
||||
ActUser: review.Reviewer,
|
||||
Content: fmt.Sprintf("%d|%s", review.Issue.Index, strings.Split(comment.Content, "\n")[0]),
|
||||
RepoID: review.Issue.RepoID,
|
||||
Repo: review.Issue.Repo,
|
||||
IsPrivate: review.Issue.Repo.IsPrivate,
|
||||
Comment: comment,
|
||||
CommentID: comment.ID,
|
||||
}
|
||||
|
||||
switch review.Type {
|
||||
case issues_model.ReviewTypeApprove:
|
||||
action.OpType = activities_model.ActionApprovePullRequest
|
||||
case issues_model.ReviewTypeReject:
|
||||
action.OpType = activities_model.ActionRejectPullRequest
|
||||
default:
|
||||
action.OpType = activities_model.ActionCommentPull
|
||||
}
|
||||
|
||||
actions = append(actions, action)
|
||||
}
|
||||
|
||||
if err := NotifyWatchers(ctx, actions...); err != nil {
|
||||
log.Error("notify watchers '%d/%d': %v", review.Reviewer.ID, review.Issue.RepoID, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (*actionNotifier) MergePullRequest(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest) {
|
||||
if err := NotifyWatchers(ctx, &activities_model.Action{
|
||||
ActUserID: doer.ID,
|
||||
ActUser: doer,
|
||||
OpType: activities_model.ActionMergePullRequest,
|
||||
Content: fmt.Sprintf("%d|%s", pr.Issue.Index, pr.Issue.Title),
|
||||
RepoID: pr.Issue.Repo.ID,
|
||||
Repo: pr.Issue.Repo,
|
||||
IsPrivate: pr.Issue.Repo.IsPrivate,
|
||||
}); err != nil {
|
||||
log.Error("NotifyWatchers [%d]: %v", pr.ID, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (*actionNotifier) AutoMergePullRequest(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest) {
|
||||
if err := NotifyWatchers(ctx, &activities_model.Action{
|
||||
ActUserID: doer.ID,
|
||||
ActUser: doer,
|
||||
OpType: activities_model.ActionAutoMergePullRequest,
|
||||
Content: fmt.Sprintf("%d|%s", pr.Issue.Index, pr.Issue.Title),
|
||||
RepoID: pr.Issue.Repo.ID,
|
||||
Repo: pr.Issue.Repo,
|
||||
IsPrivate: pr.Issue.Repo.IsPrivate,
|
||||
}); err != nil {
|
||||
log.Error("NotifyWatchers [%d]: %v", pr.ID, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (*actionNotifier) NotifyPullRevieweDismiss(ctx context.Context, doer *user_model.User, review *issues_model.Review, comment *issues_model.Comment) {
|
||||
reviewerName := review.Reviewer.Name
|
||||
if len(review.OriginalAuthor) > 0 {
|
||||
reviewerName = review.OriginalAuthor
|
||||
}
|
||||
if err := NotifyWatchers(ctx, &activities_model.Action{
|
||||
ActUserID: doer.ID,
|
||||
ActUser: doer,
|
||||
OpType: activities_model.ActionPullReviewDismissed,
|
||||
Content: fmt.Sprintf("%d|%s|%s", review.Issue.Index, reviewerName, comment.Content),
|
||||
RepoID: review.Issue.Repo.ID,
|
||||
Repo: review.Issue.Repo,
|
||||
IsPrivate: review.Issue.Repo.IsPrivate,
|
||||
CommentID: comment.ID,
|
||||
Comment: comment,
|
||||
}); err != nil {
|
||||
log.Error("NotifyWatchers [%d]: %v", review.Issue.ID, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *actionNotifier) PushCommits(ctx context.Context, pusher *user_model.User, repo *repo_model.Repository, opts *repository.PushUpdateOptions, commits *repository.PushCommits) {
|
||||
data, err := json.Marshal(commits)
|
||||
if err != nil {
|
||||
log.Error("Marshal: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
opType := activities_model.ActionCommitRepo
|
||||
|
||||
// Check it's tag push or branch.
|
||||
if opts.RefFullName.IsTag() {
|
||||
opType = activities_model.ActionPushTag
|
||||
if opts.IsDelRef() {
|
||||
opType = activities_model.ActionDeleteTag
|
||||
}
|
||||
} else if opts.IsDelRef() {
|
||||
opType = activities_model.ActionDeleteBranch
|
||||
}
|
||||
|
||||
if err = NotifyWatchers(ctx, &activities_model.Action{
|
||||
ActUserID: pusher.ID,
|
||||
ActUser: pusher,
|
||||
OpType: opType,
|
||||
Content: string(data),
|
||||
RepoID: repo.ID,
|
||||
Repo: repo,
|
||||
RefName: opts.RefFullName.String(),
|
||||
IsPrivate: repo.IsPrivate,
|
||||
}); err != nil {
|
||||
log.Error("NotifyWatchers: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *actionNotifier) CreateRef(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, refFullName git.RefName, refID string) {
|
||||
opType := activities_model.ActionCommitRepo
|
||||
if refFullName.IsTag() {
|
||||
// has sent same action in `PushCommits`, so skip it.
|
||||
return
|
||||
}
|
||||
if err := NotifyWatchers(ctx, &activities_model.Action{
|
||||
ActUserID: doer.ID,
|
||||
ActUser: doer,
|
||||
OpType: opType,
|
||||
RepoID: repo.ID,
|
||||
Repo: repo,
|
||||
IsPrivate: repo.IsPrivate,
|
||||
RefName: refFullName.String(),
|
||||
}); err != nil {
|
||||
log.Error("NotifyWatchers: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *actionNotifier) DeleteRef(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, refFullName git.RefName) {
|
||||
opType := activities_model.ActionDeleteBranch
|
||||
if refFullName.IsTag() {
|
||||
// has sent same action in `PushCommits`, so skip it.
|
||||
return
|
||||
}
|
||||
if err := NotifyWatchers(ctx, &activities_model.Action{
|
||||
ActUserID: doer.ID,
|
||||
ActUser: doer,
|
||||
OpType: opType,
|
||||
RepoID: repo.ID,
|
||||
Repo: repo,
|
||||
IsPrivate: repo.IsPrivate,
|
||||
RefName: refFullName.String(),
|
||||
}); err != nil {
|
||||
log.Error("NotifyWatchers: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *actionNotifier) SyncPushCommits(ctx context.Context, pusher *user_model.User, repo *repo_model.Repository, opts *repository.PushUpdateOptions, commits *repository.PushCommits) {
|
||||
// ignore pull sync message for pull requests refs
|
||||
// TODO: it's better to have a UI to let users chose
|
||||
if opts.RefFullName.IsPull() {
|
||||
return
|
||||
}
|
||||
|
||||
data, err := json.Marshal(commits)
|
||||
if err != nil {
|
||||
log.Error("json.Marshal: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := NotifyWatchers(ctx, &activities_model.Action{
|
||||
ActUserID: repo.OwnerID,
|
||||
ActUser: repo.MustOwner(ctx),
|
||||
OpType: activities_model.ActionMirrorSyncPush,
|
||||
RepoID: repo.ID,
|
||||
Repo: repo,
|
||||
IsPrivate: repo.IsPrivate,
|
||||
RefName: opts.RefFullName.String(),
|
||||
Content: string(data),
|
||||
}); err != nil {
|
||||
log.Error("NotifyWatchers: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *actionNotifier) SyncCreateRef(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, refFullName git.RefName, refID string) {
|
||||
// ignore pull sync message for pull requests refs
|
||||
// TODO: it's better to have a UI to let users chose
|
||||
if refFullName.IsPull() {
|
||||
return
|
||||
}
|
||||
|
||||
if err := NotifyWatchers(ctx, &activities_model.Action{
|
||||
ActUserID: repo.OwnerID,
|
||||
ActUser: repo.MustOwner(ctx),
|
||||
OpType: activities_model.ActionMirrorSyncCreate,
|
||||
RepoID: repo.ID,
|
||||
Repo: repo,
|
||||
IsPrivate: repo.IsPrivate,
|
||||
RefName: refFullName.String(),
|
||||
}); err != nil {
|
||||
log.Error("NotifyWatchers: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *actionNotifier) SyncDeleteRef(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, refFullName git.RefName) {
|
||||
// ignore pull sync message for pull requests refs
|
||||
// TODO: it's better to have a UI to let users chose
|
||||
if refFullName.IsPull() {
|
||||
return
|
||||
}
|
||||
|
||||
if err := NotifyWatchers(ctx, &activities_model.Action{
|
||||
ActUserID: repo.OwnerID,
|
||||
ActUser: repo.MustOwner(ctx),
|
||||
OpType: activities_model.ActionMirrorSyncDelete,
|
||||
RepoID: repo.ID,
|
||||
Repo: repo,
|
||||
IsPrivate: repo.IsPrivate,
|
||||
RefName: refFullName.String(),
|
||||
}); err != nil {
|
||||
log.Error("NotifyWatchers: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *actionNotifier) NewRelease(ctx context.Context, rel *repo_model.Release) {
|
||||
if err := rel.LoadAttributes(ctx); err != nil {
|
||||
log.Error("LoadAttributes: %v", err)
|
||||
return
|
||||
}
|
||||
if err := NotifyWatchers(ctx, &activities_model.Action{
|
||||
ActUserID: rel.PublisherID,
|
||||
ActUser: rel.Publisher,
|
||||
OpType: activities_model.ActionPublishRelease,
|
||||
RepoID: rel.RepoID,
|
||||
Repo: rel.Repo,
|
||||
IsPrivate: rel.Repo.IsPrivate,
|
||||
Content: rel.Title,
|
||||
RefName: git.RefNameFromTag(rel.TagName).String(), // Other functions in this file all use "refFullName.String()"
|
||||
}); err != nil {
|
||||
log.Error("NotifyWatchers: %v", err)
|
||||
}
|
||||
}
|
53
services/feed/notifier_test.go
Normal file
53
services/feed/notifier_test.go
Normal file
@@ -0,0 +1,53 @@
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package feed
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
activities_model "code.gitea.io/gitea/models/activities"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
|
||||
_ "code.gitea.io/gitea/models"
|
||||
_ "code.gitea.io/gitea/models/actions"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
unittest.MainTest(m)
|
||||
}
|
||||
|
||||
func TestRenameRepoAction(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerID: user.ID})
|
||||
repo.Owner = user
|
||||
|
||||
oldRepoName := repo.Name
|
||||
const newRepoName = "newRepoName"
|
||||
repo.Name = newRepoName
|
||||
repo.LowerName = strings.ToLower(newRepoName)
|
||||
|
||||
actionBean := &activities_model.Action{
|
||||
OpType: activities_model.ActionRenameRepo,
|
||||
ActUserID: user.ID,
|
||||
ActUser: user,
|
||||
RepoID: repo.ID,
|
||||
Repo: repo,
|
||||
IsPrivate: repo.IsPrivate,
|
||||
Content: oldRepoName,
|
||||
}
|
||||
unittest.AssertNotExistsBean(t, actionBean)
|
||||
|
||||
NewNotifier().RenameRepository(db.DefaultContext, user, repo, oldRepoName)
|
||||
|
||||
unittest.AssertExistsAndLoadBean(t, actionBean)
|
||||
unittest.CheckConsistencyFor(t, &activities_model.Action{})
|
||||
}
|
Reference in New Issue
Block a user