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

@ -6,7 +6,7 @@ DB_TYPE = mysql
HOST = 127.0.0.1:3306
NAME = testgitea
USER = root
PASSWD =
PASSWD =
SSL_MODE = disable
PATH = data/gitea.db
@ -26,14 +26,14 @@ OFFLINE_MODE = false
ENABLED = false
[service]
REGISTER_EMAIL_CONFIRM = false
ENABLE_NOTIFY_MAIL = false
DISABLE_REGISTRATION = false
ENABLE_CAPTCHA = false
REQUIRE_SIGNIN_VIEW = false
DEFAULT_KEEP_EMAIL_PRIVATE = false
REGISTER_EMAIL_CONFIRM = false
ENABLE_NOTIFY_MAIL = false
DISABLE_REGISTRATION = false
ENABLE_CAPTCHA = false
REQUIRE_SIGNIN_VIEW = false
DEFAULT_KEEP_EMAIL_PRIVATE = false
DEFAULT_ALLOW_CREATE_ORGANIZATION = true
NO_REPLY_ADDRESS = noreply.example.org
NO_REPLY_ADDRESS = noreply.example.org
[picture]
DISABLE_GRAVATAR = false
@ -53,5 +53,7 @@ LEVEL = Warn
LEVEL = Info
[security]
INSTALL_LOCK = true
SECRET_KEY = 9pCviYTWSb
INSTALL_LOCK = true
SECRET_KEY = 9pCviYTWSb
INTERNAL_TOKEN = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjE0OTU1NTE2MTh9.hhSVGOANkaKk3vfCd2jDOIww4pUk0xtg9JRde5UogyQ

View file

@ -70,20 +70,18 @@ func init() {
// repository. It implemented interface base.Actioner so that can be
// used in template render.
type Action struct {
ID int64 `xorm:"pk autoincr"`
UserID int64 `xorm:"INDEX"` // Receiver user id.
OpType ActionType
ActUserID int64 `xorm:"INDEX"` // Action user id.
ActUserName string // Action user name.
ActAvatar string `xorm:"-"`
RepoID int64 `xorm:"INDEX"`
RepoUserName string
RepoName string
RefName string
IsPrivate bool `xorm:"INDEX NOT NULL DEFAULT false"`
Content string `xorm:"TEXT"`
Created time.Time `xorm:"-"`
CreatedUnix int64 `xorm:"INDEX"`
ID int64 `xorm:"pk autoincr"`
UserID int64 `xorm:"INDEX"` // Receiver user id.
OpType ActionType
ActUserID int64 `xorm:"INDEX"` // Action user id.
ActUser *User `xorm:"-"`
RepoID int64 `xorm:"INDEX"`
Repo *Repository `xorm:"-"`
RefName string
IsPrivate bool `xorm:"INDEX NOT NULL DEFAULT false"`
Content string `xorm:"TEXT"`
Created time.Time `xorm:"-"`
CreatedUnix int64 `xorm:"INDEX"`
}
// BeforeInsert will be invoked by XORM before inserting a record
@ -106,42 +104,71 @@ func (a *Action) GetOpType() int {
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.
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
// chars.
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.
func (a *Action) GetRepoUserName() string {
return a.RepoUserName
a.loadRepo()
return a.Repo.MustOwner().Name
}
// ShortRepoUserName returns the name of the action repository owner
// trimmed to max 20 chars.
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.
func (a *Action) GetRepoName() string {
return a.RepoName
a.loadRepo()
return a.Repo.Name
}
// ShortRepoName returns the name of the action repository
// trimmed to max 33 chars.
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.
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
@ -205,13 +232,12 @@ func (a *Action) GetIssueContent() string {
func newRepoAction(e Engine, u *User, repo *Repository) (err error) {
if err = notifyWatchers(e, &Action{
ActUserID: u.ID,
ActUserName: u.Name,
OpType: ActionCreateRepo,
RepoID: repo.ID,
RepoUserName: repo.Owner.Name,
RepoName: repo.Name,
IsPrivate: repo.IsPrivate,
ActUserID: u.ID,
ActUser: u,
OpType: ActionCreateRepo,
RepoID: repo.ID,
Repo: repo,
IsPrivate: repo.IsPrivate,
}); err != nil {
return fmt.Errorf("notify watchers '%d/%d': %v", u.ID, repo.ID, err)
}
@ -227,14 +253,13 @@ func NewRepoAction(u *User, repo *Repository) (err error) {
func renameRepoAction(e Engine, actUser *User, oldRepoName string, repo *Repository) (err error) {
if err = notifyWatchers(e, &Action{
ActUserID: actUser.ID,
ActUserName: actUser.Name,
OpType: ActionRenameRepo,
RepoID: repo.ID,
RepoUserName: repo.Owner.Name,
RepoName: repo.Name,
IsPrivate: repo.IsPrivate,
Content: oldRepoName,
ActUserID: actUser.ID,
ActUser: actUser,
OpType: ActionRenameRepo,
RepoID: repo.ID,
Repo: repo,
IsPrivate: repo.IsPrivate,
Content: oldRepoName,
}); err != nil {
return fmt.Errorf("notify watchers: %v", err)
}
@ -521,15 +546,14 @@ func CommitRepoAction(opts CommitRepoActionOptions) error {
refName := git.RefEndName(opts.RefFullName)
if err = NotifyWatchers(&Action{
ActUserID: pusher.ID,
ActUserName: pusher.Name,
OpType: opType,
Content: string(data),
RepoID: repo.ID,
RepoUserName: repo.MustOwner().Name,
RepoName: repo.Name,
RefName: refName,
IsPrivate: repo.IsPrivate,
ActUserID: pusher.ID,
ActUser: pusher,
OpType: opType,
Content: string(data),
RepoID: repo.ID,
Repo: repo,
RefName: refName,
IsPrivate: repo.IsPrivate,
}); err != nil {
return fmt.Errorf("NotifyWatchers: %v", err)
}
@ -598,14 +622,13 @@ func CommitRepoAction(opts CommitRepoActionOptions) error {
func transferRepoAction(e Engine, doer, oldOwner *User, repo *Repository) (err error) {
if err = notifyWatchers(e, &Action{
ActUserID: doer.ID,
ActUserName: doer.Name,
OpType: ActionTransferRepo,
RepoID: repo.ID,
RepoUserName: repo.Owner.Name,
RepoName: repo.Name,
IsPrivate: repo.IsPrivate,
Content: path.Join(oldOwner.Name, repo.Name),
ActUserID: doer.ID,
ActUser: doer,
OpType: ActionTransferRepo,
RepoID: repo.ID,
Repo: repo,
IsPrivate: repo.IsPrivate,
Content: path.Join(oldOwner.Name, repo.Name),
}); err != nil {
return fmt.Errorf("notifyWatchers: %v", err)
}
@ -628,14 +651,13 @@ func TransferRepoAction(doer, oldOwner *User, repo *Repository) error {
func mergePullRequestAction(e Engine, doer *User, repo *Repository, issue *Issue) error {
return notifyWatchers(e, &Action{
ActUserID: doer.ID,
ActUserName: doer.Name,
OpType: ActionMergePullRequest,
Content: fmt.Sprintf("%d|%s", issue.Index, issue.Title),
RepoID: repo.ID,
RepoUserName: repo.Owner.Name,
RepoName: repo.Name,
IsPrivate: repo.IsPrivate,
ActUserID: doer.ID,
ActUser: doer,
OpType: ActionMergePullRequest,
Content: fmt.Sprintf("%d|%s", issue.Index, issue.Title),
RepoID: repo.ID,
Repo: repo,
IsPrivate: repo.IsPrivate,
})
}

View file

@ -1,6 +1,7 @@
package models
import (
"path"
"strings"
"testing"
@ -10,22 +11,21 @@ import (
)
func TestAction_GetRepoPath(t *testing.T) {
action := &Action{
RepoUserName: "username",
RepoName: "reponame",
}
assert.Equal(t, "username/reponame", action.GetRepoPath())
assert.NoError(t, PrepareTestDatabase())
repo := AssertExistsAndLoadBean(t, &Repository{}).(*Repository)
owner := AssertExistsAndLoadBean(t, &User{ID: repo.OwnerID}).(*User)
action := &Action{RepoID: repo.ID}
assert.Equal(t, path.Join(owner.Name, repo.Name), action.GetRepoPath())
}
func TestAction_GetRepoLink(t *testing.T) {
action := &Action{
RepoUserName: "username",
RepoName: "reponame",
}
assert.NoError(t, PrepareTestDatabase())
repo := AssertExistsAndLoadBean(t, &Repository{}).(*Repository)
owner := AssertExistsAndLoadBean(t, &User{ID: repo.OwnerID}).(*User)
action := &Action{RepoID: repo.ID}
setting.AppSubURL = "/suburl/"
assert.Equal(t, "/suburl/username/reponame", action.GetRepoLink())
setting.AppSubURL = ""
assert.Equal(t, "/username/reponame", action.GetRepoLink())
expected := path.Join(setting.AppSubURL, owner.Name, repo.Name)
assert.Equal(t, expected, action.GetRepoLink())
}
func TestNewRepoAction(t *testing.T) {
@ -36,13 +36,12 @@ func TestNewRepoAction(t *testing.T) {
repo.Owner = user
actionBean := &Action{
OpType: ActionCreateRepo,
ActUserID: user.ID,
RepoID: repo.ID,
ActUserName: user.Name,
RepoName: repo.Name,
RepoUserName: repo.Owner.Name,
IsPrivate: repo.IsPrivate,
OpType: ActionCreateRepo,
ActUserID: user.ID,
RepoID: repo.ID,
ActUser: user,
Repo: repo,
IsPrivate: repo.IsPrivate,
}
AssertNotExistsBean(t, actionBean)
@ -64,14 +63,13 @@ func TestRenameRepoAction(t *testing.T) {
repo.LowerName = strings.ToLower(newRepoName)
actionBean := &Action{
OpType: ActionRenameRepo,
ActUserID: user.ID,
ActUserName: user.Name,
RepoID: repo.ID,
RepoName: repo.Name,
RepoUserName: repo.Owner.Name,
IsPrivate: repo.IsPrivate,
Content: oldRepoName,
OpType: ActionRenameRepo,
ActUserID: user.ID,
ActUser: user,
RepoID: repo.ID,
Repo: repo,
IsPrivate: repo.IsPrivate,
Content: oldRepoName,
}
AssertNotExistsBean(t, actionBean)
assert.NoError(t, RenameRepoAction(user, oldRepoName, repo))
@ -232,13 +230,13 @@ func TestCommitRepoAction(t *testing.T) {
pushCommits.Len = len(pushCommits.Commits)
actionBean := &Action{
OpType: ActionCommitRepo,
ActUserID: user.ID,
ActUserName: user.Name,
RepoID: repo.ID,
RepoName: repo.Name,
RefName: "refName",
IsPrivate: repo.IsPrivate,
OpType: ActionCommitRepo,
ActUserID: user.ID,
ActUser: user,
RepoID: repo.ID,
Repo: repo,
RefName: "refName",
IsPrivate: repo.IsPrivate,
}
AssertNotExistsBean(t, actionBean)
assert.NoError(t, CommitRepoAction(CommitRepoActionOptions{
@ -265,13 +263,12 @@ func TestTransferRepoAction(t *testing.T) {
repo.Owner = user4
actionBean := &Action{
OpType: ActionTransferRepo,
ActUserID: user2.ID,
ActUserName: user2.Name,
RepoID: repo.ID,
RepoName: repo.Name,
RepoUserName: repo.Owner.Name,
IsPrivate: repo.IsPrivate,
OpType: ActionTransferRepo,
ActUserID: user2.ID,
ActUser: user2,
RepoID: repo.ID,
Repo: repo,
IsPrivate: repo.IsPrivate,
}
AssertNotExistsBean(t, actionBean)
assert.NoError(t, TransferRepoAction(user2, user2, repo))
@ -290,13 +287,12 @@ func TestMergePullRequestAction(t *testing.T) {
issue := AssertExistsAndLoadBean(t, &Issue{ID: 3, RepoID: repo.ID}).(*Issue)
actionBean := &Action{
OpType: ActionMergePullRequest,
ActUserID: user.ID,
ActUserName: user.Name,
RepoID: repo.ID,
RepoName: repo.Name,
RepoUserName: repo.Owner.Name,
IsPrivate: repo.IsPrivate,
OpType: ActionMergePullRequest,
ActUserID: user.ID,
ActUser: user,
RepoID: repo.ID,
Repo: repo,
IsPrivate: repo.IsPrivate,
}
AssertNotExistsBean(t, actionBean)
assert.NoError(t, MergePullRequestAction(user, repo, issue))

View file

@ -162,11 +162,5 @@ func (team *Team) checkForConsistency(t *testing.T) {
func (action *Action) checkForConsistency(t *testing.T) {
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, 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
op_type: 12 # close issue
act_user_id: 2
act_user_name: user2
repo_id: 2
repo_user_name: user2
repo_name: repo2
is_private: true
-
@ -14,10 +11,7 @@
user_id: 3
op_type: 2 # rename repo
act_user_id: 3
act_user_name: user3
repo_id: 3
repo_user_name: user3
repo_name: repo3
is_private: true
content: oldRepoName
@ -26,8 +20,5 @@
user_id: 11
op_type: 1 # create repo
act_user_id: 11
act_user_name: user11
repo_id: 9
repo_user_name: user11
repo_name: repo9
is_private: false

View file

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

View file

@ -329,13 +329,12 @@ func createComment(e *xorm.Session, opts *CreateCommentOptions) (_ *Comment, err
// 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 := &Action{
ActUserID: opts.Doer.ID,
ActUserName: opts.Doer.Name,
Content: fmt.Sprintf("%d|%s", opts.Issue.Index, strings.Split(opts.Content, "\n")[0]),
RepoID: opts.Repo.ID,
RepoUserName: opts.Repo.Owner.Name,
RepoName: opts.Repo.Name,
IsPrivate: opts.Repo.IsPrivate,
ActUserID: opts.Doer.ID,
ActUser: opts.Doer,
Content: fmt.Sprintf("%d|%s", opts.Issue.Index, strings.Split(opts.Content, "\n")[0]),
RepoID: opts.Repo.ID,
Repo: opts.Repo,
IsPrivate: opts.Repo.IsPrivate,
}
// Check comment type.

View file

@ -114,6 +114,8 @@ var migrations = []Migration{
NewMigration("add field for login source synchronization", addLoginSourceSyncEnabledColumn),
// v32 -> v33
NewMigration("add units for team", addUnitsToRepoTeam),
// v33 -> v34
NewMigration("remove columns from action", removeActionColumns),
}
// 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

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

View file

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