Fix activity feed (#1779)

* Fix activity feed

Preserve actions after user/repo name change

* Add missing comment

* Fix migration, and remove fields completely

* Tests
This commit is contained in:
Ethan Koenig 2017-05-25 21:38:18 -04:00 committed by Lunny Xiao
parent 03912ce014
commit 0c332f0480
11 changed files with 238 additions and 165 deletions

View File

@ -55,3 +55,5 @@ LEVEL = Info
[security] [security]
INSTALL_LOCK = true INSTALL_LOCK = true
SECRET_KEY = 9pCviYTWSb SECRET_KEY = 9pCviYTWSb
INTERNAL_TOKEN = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjE0OTU1NTE2MTh9.hhSVGOANkaKk3vfCd2jDOIww4pUk0xtg9JRde5UogyQ

View File

@ -74,11 +74,9 @@ type Action struct {
UserID int64 `xorm:"INDEX"` // Receiver user id. UserID int64 `xorm:"INDEX"` // Receiver user id.
OpType ActionType OpType ActionType
ActUserID int64 `xorm:"INDEX"` // Action user id. ActUserID int64 `xorm:"INDEX"` // Action user id.
ActUserName string // Action user name. ActUser *User `xorm:"-"`
ActAvatar string `xorm:"-"`
RepoID int64 `xorm:"INDEX"` RepoID int64 `xorm:"INDEX"`
RepoUserName string Repo *Repository `xorm:"-"`
RepoName string
RefName string RefName string
IsPrivate bool `xorm:"INDEX NOT NULL DEFAULT false"` IsPrivate bool `xorm:"INDEX NOT NULL DEFAULT false"`
Content string `xorm:"TEXT"` Content string `xorm:"TEXT"`
@ -106,42 +104,71 @@ func (a *Action) GetOpType() int {
return int(a.OpType) return int(a.OpType)
} }
func (a *Action) loadActUser() {
if a.ActUser != nil {
return
}
var err error
a.ActUser, err = GetUserByID(a.ActUserID)
if err == nil {
return
} else if IsErrUserNotExist(err) {
a.ActUser = NewGhostUser()
} else {
log.Error(4, "GetUserByID(%d): %v", a.ActUserID, err)
}
}
func (a *Action) loadRepo() {
if a.ActUser != nil {
return
}
var err error
a.Repo, err = GetRepositoryByID(a.RepoID)
if err != nil {
log.Error(4, "GetRepositoryByID(%d): %v", a.RepoID, err)
}
}
// GetActUserName gets the action's user name. // GetActUserName gets the action's user name.
func (a *Action) GetActUserName() string { func (a *Action) GetActUserName() string {
return a.ActUserName a.loadActUser()
return a.ActUser.Name
} }
// ShortActUserName gets the action's user name trimmed to max 20 // ShortActUserName gets the action's user name trimmed to max 20
// chars. // chars.
func (a *Action) ShortActUserName() string { func (a *Action) ShortActUserName() string {
return base.EllipsisString(a.ActUserName, 20) return base.EllipsisString(a.GetActUserName(), 20)
} }
// GetRepoUserName returns the name of the action repository owner. // GetRepoUserName returns the name of the action repository owner.
func (a *Action) GetRepoUserName() string { func (a *Action) GetRepoUserName() string {
return a.RepoUserName a.loadRepo()
return a.Repo.MustOwner().Name
} }
// ShortRepoUserName returns the name of the action repository owner // ShortRepoUserName returns the name of the action repository owner
// trimmed to max 20 chars. // trimmed to max 20 chars.
func (a *Action) ShortRepoUserName() string { func (a *Action) ShortRepoUserName() string {
return base.EllipsisString(a.RepoUserName, 20) return base.EllipsisString(a.GetRepoUserName(), 20)
} }
// GetRepoName returns the name of the action repository. // GetRepoName returns the name of the action repository.
func (a *Action) GetRepoName() string { func (a *Action) GetRepoName() string {
return a.RepoName a.loadRepo()
return a.Repo.Name
} }
// ShortRepoName returns the name of the action repository // ShortRepoName returns the name of the action repository
// trimmed to max 33 chars. // trimmed to max 33 chars.
func (a *Action) ShortRepoName() string { func (a *Action) ShortRepoName() string {
return base.EllipsisString(a.RepoName, 33) return base.EllipsisString(a.GetRepoName(), 33)
} }
// GetRepoPath returns the virtual path to the action repository. // GetRepoPath returns the virtual path to the action repository.
func (a *Action) GetRepoPath() string { func (a *Action) GetRepoPath() string {
return path.Join(a.RepoUserName, a.RepoName) return path.Join(a.GetRepoUserName(), a.GetRepoName())
} }
// ShortRepoPath returns the virtual path to the action repository // ShortRepoPath returns the virtual path to the action repository
@ -206,11 +233,10 @@ func (a *Action) GetIssueContent() string {
func newRepoAction(e Engine, u *User, repo *Repository) (err error) { func newRepoAction(e Engine, u *User, repo *Repository) (err error) {
if err = notifyWatchers(e, &Action{ if err = notifyWatchers(e, &Action{
ActUserID: u.ID, ActUserID: u.ID,
ActUserName: u.Name, ActUser: u,
OpType: ActionCreateRepo, OpType: ActionCreateRepo,
RepoID: repo.ID, RepoID: repo.ID,
RepoUserName: repo.Owner.Name, Repo: repo,
RepoName: repo.Name,
IsPrivate: repo.IsPrivate, IsPrivate: repo.IsPrivate,
}); err != nil { }); err != nil {
return fmt.Errorf("notify watchers '%d/%d': %v", u.ID, repo.ID, err) return fmt.Errorf("notify watchers '%d/%d': %v", u.ID, repo.ID, err)
@ -228,11 +254,10 @@ func NewRepoAction(u *User, repo *Repository) (err error) {
func renameRepoAction(e Engine, actUser *User, oldRepoName string, repo *Repository) (err error) { func renameRepoAction(e Engine, actUser *User, oldRepoName string, repo *Repository) (err error) {
if err = notifyWatchers(e, &Action{ if err = notifyWatchers(e, &Action{
ActUserID: actUser.ID, ActUserID: actUser.ID,
ActUserName: actUser.Name, ActUser: actUser,
OpType: ActionRenameRepo, OpType: ActionRenameRepo,
RepoID: repo.ID, RepoID: repo.ID,
RepoUserName: repo.Owner.Name, Repo: repo,
RepoName: repo.Name,
IsPrivate: repo.IsPrivate, IsPrivate: repo.IsPrivate,
Content: oldRepoName, Content: oldRepoName,
}); err != nil { }); err != nil {
@ -522,12 +547,11 @@ func CommitRepoAction(opts CommitRepoActionOptions) error {
refName := git.RefEndName(opts.RefFullName) refName := git.RefEndName(opts.RefFullName)
if err = NotifyWatchers(&Action{ if err = NotifyWatchers(&Action{
ActUserID: pusher.ID, ActUserID: pusher.ID,
ActUserName: pusher.Name, ActUser: pusher,
OpType: opType, OpType: opType,
Content: string(data), Content: string(data),
RepoID: repo.ID, RepoID: repo.ID,
RepoUserName: repo.MustOwner().Name, Repo: repo,
RepoName: repo.Name,
RefName: refName, RefName: refName,
IsPrivate: repo.IsPrivate, IsPrivate: repo.IsPrivate,
}); err != nil { }); err != nil {
@ -599,11 +623,10 @@ func CommitRepoAction(opts CommitRepoActionOptions) error {
func transferRepoAction(e Engine, doer, oldOwner *User, repo *Repository) (err error) { func transferRepoAction(e Engine, doer, oldOwner *User, repo *Repository) (err error) {
if err = notifyWatchers(e, &Action{ if err = notifyWatchers(e, &Action{
ActUserID: doer.ID, ActUserID: doer.ID,
ActUserName: doer.Name, ActUser: doer,
OpType: ActionTransferRepo, OpType: ActionTransferRepo,
RepoID: repo.ID, RepoID: repo.ID,
RepoUserName: repo.Owner.Name, Repo: repo,
RepoName: repo.Name,
IsPrivate: repo.IsPrivate, IsPrivate: repo.IsPrivate,
Content: path.Join(oldOwner.Name, repo.Name), Content: path.Join(oldOwner.Name, repo.Name),
}); err != nil { }); err != nil {
@ -629,12 +652,11 @@ func TransferRepoAction(doer, oldOwner *User, repo *Repository) error {
func mergePullRequestAction(e Engine, doer *User, repo *Repository, issue *Issue) error { func mergePullRequestAction(e Engine, doer *User, repo *Repository, issue *Issue) error {
return notifyWatchers(e, &Action{ return notifyWatchers(e, &Action{
ActUserID: doer.ID, ActUserID: doer.ID,
ActUserName: doer.Name, ActUser: doer,
OpType: ActionMergePullRequest, OpType: ActionMergePullRequest,
Content: fmt.Sprintf("%d|%s", issue.Index, issue.Title), Content: fmt.Sprintf("%d|%s", issue.Index, issue.Title),
RepoID: repo.ID, RepoID: repo.ID,
RepoUserName: repo.Owner.Name, Repo: repo,
RepoName: repo.Name,
IsPrivate: repo.IsPrivate, IsPrivate: repo.IsPrivate,
}) })
} }

View File

@ -1,6 +1,7 @@
package models package models
import ( import (
"path"
"strings" "strings"
"testing" "testing"
@ -10,22 +11,21 @@ import (
) )
func TestAction_GetRepoPath(t *testing.T) { func TestAction_GetRepoPath(t *testing.T) {
action := &Action{ assert.NoError(t, PrepareTestDatabase())
RepoUserName: "username", repo := AssertExistsAndLoadBean(t, &Repository{}).(*Repository)
RepoName: "reponame", owner := AssertExistsAndLoadBean(t, &User{ID: repo.OwnerID}).(*User)
} action := &Action{RepoID: repo.ID}
assert.Equal(t, "username/reponame", action.GetRepoPath()) assert.Equal(t, path.Join(owner.Name, repo.Name), action.GetRepoPath())
} }
func TestAction_GetRepoLink(t *testing.T) { func TestAction_GetRepoLink(t *testing.T) {
action := &Action{ assert.NoError(t, PrepareTestDatabase())
RepoUserName: "username", repo := AssertExistsAndLoadBean(t, &Repository{}).(*Repository)
RepoName: "reponame", owner := AssertExistsAndLoadBean(t, &User{ID: repo.OwnerID}).(*User)
} action := &Action{RepoID: repo.ID}
setting.AppSubURL = "/suburl/" setting.AppSubURL = "/suburl/"
assert.Equal(t, "/suburl/username/reponame", action.GetRepoLink()) expected := path.Join(setting.AppSubURL, owner.Name, repo.Name)
setting.AppSubURL = "" assert.Equal(t, expected, action.GetRepoLink())
assert.Equal(t, "/username/reponame", action.GetRepoLink())
} }
func TestNewRepoAction(t *testing.T) { func TestNewRepoAction(t *testing.T) {
@ -39,9 +39,8 @@ func TestNewRepoAction(t *testing.T) {
OpType: ActionCreateRepo, OpType: ActionCreateRepo,
ActUserID: user.ID, ActUserID: user.ID,
RepoID: repo.ID, RepoID: repo.ID,
ActUserName: user.Name, ActUser: user,
RepoName: repo.Name, Repo: repo,
RepoUserName: repo.Owner.Name,
IsPrivate: repo.IsPrivate, IsPrivate: repo.IsPrivate,
} }
@ -66,10 +65,9 @@ func TestRenameRepoAction(t *testing.T) {
actionBean := &Action{ actionBean := &Action{
OpType: ActionRenameRepo, OpType: ActionRenameRepo,
ActUserID: user.ID, ActUserID: user.ID,
ActUserName: user.Name, ActUser: user,
RepoID: repo.ID, RepoID: repo.ID,
RepoName: repo.Name, Repo: repo,
RepoUserName: repo.Owner.Name,
IsPrivate: repo.IsPrivate, IsPrivate: repo.IsPrivate,
Content: oldRepoName, Content: oldRepoName,
} }
@ -234,9 +232,9 @@ func TestCommitRepoAction(t *testing.T) {
actionBean := &Action{ actionBean := &Action{
OpType: ActionCommitRepo, OpType: ActionCommitRepo,
ActUserID: user.ID, ActUserID: user.ID,
ActUserName: user.Name, ActUser: user,
RepoID: repo.ID, RepoID: repo.ID,
RepoName: repo.Name, Repo: repo,
RefName: "refName", RefName: "refName",
IsPrivate: repo.IsPrivate, IsPrivate: repo.IsPrivate,
} }
@ -267,10 +265,9 @@ func TestTransferRepoAction(t *testing.T) {
actionBean := &Action{ actionBean := &Action{
OpType: ActionTransferRepo, OpType: ActionTransferRepo,
ActUserID: user2.ID, ActUserID: user2.ID,
ActUserName: user2.Name, ActUser: user2,
RepoID: repo.ID, RepoID: repo.ID,
RepoName: repo.Name, Repo: repo,
RepoUserName: repo.Owner.Name,
IsPrivate: repo.IsPrivate, IsPrivate: repo.IsPrivate,
} }
AssertNotExistsBean(t, actionBean) AssertNotExistsBean(t, actionBean)
@ -292,10 +289,9 @@ func TestMergePullRequestAction(t *testing.T) {
actionBean := &Action{ actionBean := &Action{
OpType: ActionMergePullRequest, OpType: ActionMergePullRequest,
ActUserID: user.ID, ActUserID: user.ID,
ActUserName: user.Name, ActUser: user,
RepoID: repo.ID, RepoID: repo.ID,
RepoName: repo.Name, Repo: repo,
RepoUserName: repo.Owner.Name,
IsPrivate: repo.IsPrivate, IsPrivate: repo.IsPrivate,
} }
AssertNotExistsBean(t, actionBean) AssertNotExistsBean(t, actionBean)

View File

@ -162,11 +162,5 @@ func (team *Team) checkForConsistency(t *testing.T) {
func (action *Action) checkForConsistency(t *testing.T) { func (action *Action) checkForConsistency(t *testing.T) {
repo := AssertExistsAndLoadBean(t, &Repository{ID: action.RepoID}).(*Repository) repo := AssertExistsAndLoadBean(t, &Repository{ID: action.RepoID}).(*Repository)
owner := AssertExistsAndLoadBean(t, &User{ID: repo.OwnerID}).(*User)
actor := AssertExistsAndLoadBean(t, &User{ID: action.ActUserID}).(*User)
assert.Equal(t, repo.Name, action.RepoName, "action: %+v", action)
assert.Equal(t, repo.IsPrivate, action.IsPrivate, "action: %+v", action) assert.Equal(t, repo.IsPrivate, action.IsPrivate, "action: %+v", action)
assert.Equal(t, owner.Name, action.RepoUserName, "action: %+v", action)
assert.Equal(t, actor.Name, action.ActUserName, "action: %+v", action)
} }

View File

@ -3,10 +3,7 @@
user_id: 2 user_id: 2
op_type: 12 # close issue op_type: 12 # close issue
act_user_id: 2 act_user_id: 2
act_user_name: user2
repo_id: 2 repo_id: 2
repo_user_name: user2
repo_name: repo2
is_private: true is_private: true
- -
@ -14,10 +11,7 @@
user_id: 3 user_id: 3
op_type: 2 # rename repo op_type: 2 # rename repo
act_user_id: 3 act_user_id: 3
act_user_name: user3
repo_id: 3 repo_id: 3
repo_user_name: user3
repo_name: repo3
is_private: true is_private: true
content: oldRepoName content: oldRepoName
@ -26,8 +20,5 @@
user_id: 11 user_id: 11
op_type: 1 # create repo op_type: 1 # create repo
act_user_id: 11 act_user_id: 11
act_user_name: user11
repo_id: 9 repo_id: 9
repo_user_name: user11
repo_name: repo9
is_private: false is_private: false

View File

@ -919,12 +919,11 @@ func NewIssue(repo *Repository, issue *Issue, labelIDs []int64, uuids []string)
if err = NotifyWatchers(&Action{ if err = NotifyWatchers(&Action{
ActUserID: issue.Poster.ID, ActUserID: issue.Poster.ID,
ActUserName: issue.Poster.Name, ActUser: issue.Poster,
OpType: ActionCreateIssue, OpType: ActionCreateIssue,
Content: fmt.Sprintf("%d|%s", issue.Index, issue.Title), Content: fmt.Sprintf("%d|%s", issue.Index, issue.Title),
RepoID: repo.ID, RepoID: repo.ID,
RepoUserName: repo.Owner.Name, Repo: repo,
RepoName: repo.Name,
IsPrivate: repo.IsPrivate, IsPrivate: repo.IsPrivate,
}); err != nil { }); err != nil {
log.Error(4, "NotifyWatchers: %v", err) log.Error(4, "NotifyWatchers: %v", err)

View File

@ -330,11 +330,10 @@ func createComment(e *xorm.Session, opts *CreateCommentOptions) (_ *Comment, err
// This object will be used to notify watchers in the end of function. // This object will be used to notify watchers in the end of function.
act := &Action{ act := &Action{
ActUserID: opts.Doer.ID, ActUserID: opts.Doer.ID,
ActUserName: opts.Doer.Name, ActUser: opts.Doer,
Content: fmt.Sprintf("%d|%s", opts.Issue.Index, strings.Split(opts.Content, "\n")[0]), Content: fmt.Sprintf("%d|%s", opts.Issue.Index, strings.Split(opts.Content, "\n")[0]),
RepoID: opts.Repo.ID, RepoID: opts.Repo.ID,
RepoUserName: opts.Repo.Owner.Name, Repo: opts.Repo,
RepoName: opts.Repo.Name,
IsPrivate: opts.Repo.IsPrivate, IsPrivate: opts.Repo.IsPrivate,
} }

View File

@ -114,6 +114,8 @@ var migrations = []Migration{
NewMigration("add field for login source synchronization", addLoginSourceSyncEnabledColumn), NewMigration("add field for login source synchronization", addLoginSourceSyncEnabledColumn),
// v32 -> v33 // v32 -> v33
NewMigration("add units for team", addUnitsToRepoTeam), NewMigration("add units for team", addUnitsToRepoTeam),
// v33 -> v34
NewMigration("remove columns from action", removeActionColumns),
} }
// Migrate database to current version // Migrate database to current version

44
models/migrations/v34.go Normal file
View File

@ -0,0 +1,44 @@
// Copyright 2017 Gitea. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package migrations
import (
"fmt"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"github.com/go-xorm/xorm"
)
// ActionV34 describes the removed fields
type ActionV34 struct {
ActUserName string `xorm:"-"`
RepoUserName string `xorm:"-"`
RepoName string `xorm:"-"`
}
// TableName will be invoked by XORM to customize the table name
func (*ActionV34) TableName() string {
return "action"
}
func removeActionColumns(x *xorm.Engine) error {
switch {
case setting.UseSQLite3:
log.Warn("Unable to drop columns in SQLite")
case setting.UseMySQL, setting.UsePostgreSQL, setting.UseMSSQL, setting.UseTiDB:
if _, err := x.Exec("ALTER TABLE action DROP COLUMN act_user_name"); err != nil {
return fmt.Errorf("DROP COLUMN act_user_name: %v", err)
} else if _, err = x.Exec("ALTER TABLE action DROP COLUMN repo_user_name"); err != nil {
return fmt.Errorf("DROP COLUMN repo_user_name: %v", err)
} else if _, err = x.Exec("ALTER TABLE action DROP COLUMN repo_name"); err != nil {
return fmt.Errorf("DROP COLUMN repo_name: %v", err)
}
default:
log.Fatal(4, "Unrecognized DB")
}
return nil
}

View File

@ -636,12 +636,11 @@ func NewPullRequest(repo *Repository, pull *Issue, labelIDs []int64, uuids []str
if err = NotifyWatchers(&Action{ if err = NotifyWatchers(&Action{
ActUserID: pull.Poster.ID, ActUserID: pull.Poster.ID,
ActUserName: pull.Poster.Name, ActUser: pull.Poster,
OpType: ActionCreatePullRequest, OpType: ActionCreatePullRequest,
Content: fmt.Sprintf("%d|%s", pull.Index, pull.Title), Content: fmt.Sprintf("%d|%s", pull.Index, pull.Title),
RepoID: repo.ID, RepoID: repo.ID,
RepoUserName: repo.Owner.Name, Repo: repo,
RepoName: repo.Name,
IsPrivate: repo.IsPrivate, IsPrivate: repo.IsPrivate,
}); err != nil { }); err != nil {
log.Error(4, "NotifyWatchers: %v", err) log.Error(4, "NotifyWatchers: %v", err)

View File

@ -65,25 +65,50 @@ func retrieveFeeds(ctx *context.Context, ctxUser *models.User, userID, offset in
// Check access of private repositories. // Check access of private repositories.
feeds := make([]*models.Action, 0, len(actions)) feeds := make([]*models.Action, 0, len(actions))
unameAvatars := map[string]string{ userCache := map[int64]*models.User{ctxUser.ID: ctxUser}
ctxUser.Name: ctxUser.RelAvatarLink(), repoCache := map[int64]*models.Repository{}
}
for _, act := range actions { for _, act := range actions {
// Cache results to reduce queries. // Cache results to reduce queries.
_, ok := unameAvatars[act.ActUserName] u, ok := userCache[act.ActUserID]
if !ok { if !ok {
u, err := models.GetUserByName(act.ActUserName) u, err = models.GetUserByID(act.ActUserID)
if err != nil { if err != nil {
if models.IsErrUserNotExist(err) { if models.IsErrUserNotExist(err) {
continue continue
} }
ctx.Handle(500, "GetUserByName", err) ctx.Handle(500, "GetUserByID", err)
return return
} }
unameAvatars[act.ActUserName] = u.RelAvatarLink() userCache[act.ActUserID] = u
} }
act.ActUser = u
repo, ok := repoCache[act.RepoID]
if !ok {
repo, err = models.GetRepositoryByID(act.RepoID)
if err != nil {
if models.IsErrRepoNotExist(err) {
continue
}
ctx.Handle(500, "GetRepositoryByID", err)
return
}
}
act.Repo = repo
repoOwner, ok := userCache[repo.OwnerID]
if !ok {
repoOwner, err = models.GetUserByID(repo.OwnerID)
if err != nil {
if models.IsErrUserNotExist(err) {
continue
}
ctx.Handle(500, "GetUserByID", err)
return
}
}
repo.Owner = repoOwner
act.ActAvatar = unameAvatars[act.ActUserName]
feeds = append(feeds, act) feeds = append(feeds, act)
} }
ctx.Data["Feeds"] = feeds ctx.Data["Feeds"] = feeds