* support for repos * refactor * advertise the feeds via meta tags * allow feed suffix and feed header * optimize performancetags/v1.18.0-dev
@@ -328,7 +328,7 @@ type GetFeedsOptions struct { | |||
} | |||
// GetFeeds returns actions according to the provided options | |||
func GetFeeds(opts GetFeedsOptions) ([]*Action, error) { | |||
func GetFeeds(ctx context.Context, opts GetFeedsOptions) (ActionList, error) { | |||
if opts.RequestedUser == nil && opts.RequestedTeam == nil && opts.RequestedRepo == nil { | |||
return nil, fmt.Errorf("need at least one of these filters: RequestedUser, RequestedTeam, RequestedRepo") | |||
} | |||
@@ -338,7 +338,8 @@ func GetFeeds(opts GetFeedsOptions) ([]*Action, error) { | |||
return nil, err | |||
} | |||
sess := db.GetEngine(db.DefaultContext).Where(cond) | |||
e := db.GetEngine(ctx) | |||
sess := e.Where(cond) | |||
opts.SetDefaultValues() | |||
sess = db.SetSessionPagination(sess, &opts) | |||
@@ -349,7 +350,7 @@ func GetFeeds(opts GetFeedsOptions) ([]*Action, error) { | |||
return nil, fmt.Errorf("Find: %v", err) | |||
} | |||
if err := ActionList(actions).LoadAttributes(); err != nil { | |||
if err := ActionList(actions).loadAttributes(e); err != nil { | |||
return nil, fmt.Errorf("LoadAttributes: %v", err) | |||
} | |||
@@ -25,7 +25,7 @@ func (actions ActionList) getUserIDs() []int64 { | |||
return keysInt64(userIDs) | |||
} | |||
func (actions ActionList) loadUsers(e db.Engine) ([]*user_model.User, error) { | |||
func (actions ActionList) loadUsers(e db.Engine) (map[int64]*user_model.User, error) { | |||
if len(actions) == 0 { | |||
return nil, nil | |||
} | |||
@@ -42,12 +42,7 @@ func (actions ActionList) loadUsers(e db.Engine) ([]*user_model.User, error) { | |||
for _, action := range actions { | |||
action.ActUser = userMaps[action.ActUserID] | |||
} | |||
return valuesUser(userMaps), nil | |||
} | |||
// LoadUsers loads actions' all users | |||
func (actions ActionList) LoadUsers() ([]*user_model.User, error) { | |||
return actions.loadUsers(db.GetEngine(db.DefaultContext)) | |||
return userMaps, nil | |||
} | |||
func (actions ActionList) getRepoIDs() []int64 { | |||
@@ -60,45 +55,57 @@ func (actions ActionList) getRepoIDs() []int64 { | |||
return keysInt64(repoIDs) | |||
} | |||
func (actions ActionList) loadRepositories(e db.Engine) ([]*repo_model.Repository, error) { | |||
func (actions ActionList) loadRepositories(e db.Engine) error { | |||
if len(actions) == 0 { | |||
return nil, nil | |||
return nil | |||
} | |||
repoIDs := actions.getRepoIDs() | |||
repoMaps := make(map[int64]*repo_model.Repository, len(repoIDs)) | |||
err := e. | |||
In("id", repoIDs). | |||
Find(&repoMaps) | |||
err := e.In("id", repoIDs).Find(&repoMaps) | |||
if err != nil { | |||
return nil, fmt.Errorf("find repository: %v", err) | |||
return fmt.Errorf("find repository: %v", err) | |||
} | |||
for _, action := range actions { | |||
action.Repo = repoMaps[action.RepoID] | |||
} | |||
return valuesRepository(repoMaps), nil | |||
} | |||
// LoadRepositories loads actions' all repositories | |||
func (actions ActionList) LoadRepositories() ([]*repo_model.Repository, error) { | |||
return actions.loadRepositories(db.GetEngine(db.DefaultContext)) | |||
return nil | |||
} | |||
// loadAttributes loads all attributes | |||
func (actions ActionList) loadAttributes(e db.Engine) (err error) { | |||
if _, err = actions.loadUsers(e); err != nil { | |||
return | |||
func (actions ActionList) loadRepoOwner(e db.Engine, userMap map[int64]*user_model.User) (err error) { | |||
if userMap == nil { | |||
userMap = make(map[int64]*user_model.User) | |||
} | |||
if _, err = actions.loadRepositories(e); err != nil { | |||
return | |||
for _, action := range actions { | |||
repoOwner, ok := userMap[action.Repo.OwnerID] | |||
if !ok { | |||
repoOwner, err = user_model.GetUserByID(action.Repo.OwnerID) | |||
if err != nil { | |||
if user_model.IsErrUserNotExist(err) { | |||
continue | |||
} | |||
return err | |||
} | |||
userMap[repoOwner.ID] = repoOwner | |||
} | |||
action.Repo.Owner = repoOwner | |||
} | |||
return nil | |||
} | |||
// LoadAttributes loads attributes of the actions | |||
func (actions ActionList) LoadAttributes() error { | |||
return actions.loadAttributes(db.GetEngine(db.DefaultContext)) | |||
// loadAttributes loads all attributes | |||
func (actions ActionList) loadAttributes(e db.Engine) error { | |||
userMap, err := actions.loadUsers(e) | |||
if err != nil { | |||
return err | |||
} | |||
if err := actions.loadRepositories(e); err != nil { | |||
return err | |||
} | |||
return actions.loadRepoOwner(e, userMap) | |||
} |
@@ -8,6 +8,7 @@ import ( | |||
"path" | |||
"testing" | |||
"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" | |||
@@ -39,7 +40,7 @@ func TestGetFeeds(t *testing.T) { | |||
assert.NoError(t, unittest.PrepareTestDatabase()) | |||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User) | |||
actions, err := GetFeeds(GetFeedsOptions{ | |||
actions, err := GetFeeds(db.DefaultContext, GetFeedsOptions{ | |||
RequestedUser: user, | |||
Actor: user, | |||
IncludePrivate: true, | |||
@@ -52,7 +53,7 @@ func TestGetFeeds(t *testing.T) { | |||
assert.EqualValues(t, user.ID, actions[0].UserID) | |||
} | |||
actions, err = GetFeeds(GetFeedsOptions{ | |||
actions, err = GetFeeds(db.DefaultContext, GetFeedsOptions{ | |||
RequestedUser: user, | |||
Actor: user, | |||
IncludePrivate: false, | |||
@@ -62,13 +63,54 @@ func TestGetFeeds(t *testing.T) { | |||
assert.Len(t, actions, 0) | |||
} | |||
func TestGetFeedsForRepos(t *testing.T) { | |||
assert.NoError(t, unittest.PrepareTestDatabase()) | |||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User) | |||
privRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}).(*repo_model.Repository) | |||
pubRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 8}).(*repo_model.Repository) | |||
// private repo & no login | |||
actions, err := GetFeeds(db.DefaultContext, GetFeedsOptions{ | |||
RequestedRepo: privRepo, | |||
IncludePrivate: true, | |||
}) | |||
assert.NoError(t, err) | |||
assert.Len(t, actions, 0) | |||
// public repo & no login | |||
actions, err = GetFeeds(db.DefaultContext, GetFeedsOptions{ | |||
RequestedRepo: pubRepo, | |||
IncludePrivate: true, | |||
}) | |||
assert.NoError(t, err) | |||
assert.Len(t, actions, 1) | |||
// private repo and login | |||
actions, err = GetFeeds(db.DefaultContext, GetFeedsOptions{ | |||
RequestedRepo: privRepo, | |||
IncludePrivate: true, | |||
Actor: user, | |||
}) | |||
assert.NoError(t, err) | |||
assert.Len(t, actions, 1) | |||
// public repo & login | |||
actions, err = GetFeeds(db.DefaultContext, GetFeedsOptions{ | |||
RequestedRepo: pubRepo, | |||
IncludePrivate: true, | |||
Actor: user, | |||
}) | |||
assert.NoError(t, err) | |||
assert.Len(t, actions, 1) | |||
} | |||
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_model.User) | |||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User) | |||
actions, err := GetFeeds(GetFeedsOptions{ | |||
actions, err := GetFeeds(db.DefaultContext, GetFeedsOptions{ | |||
RequestedUser: org, | |||
Actor: user, | |||
IncludePrivate: true, | |||
@@ -82,7 +124,7 @@ func TestGetFeeds2(t *testing.T) { | |||
assert.EqualValues(t, org.ID, actions[0].UserID) | |||
} | |||
actions, err = GetFeeds(GetFeedsOptions{ | |||
actions, err = GetFeeds(db.DefaultContext, GetFeedsOptions{ | |||
RequestedUser: org, | |||
Actor: user, | |||
IncludePrivate: false, |
@@ -3,7 +3,7 @@ | |||
user_id: 2 | |||
op_type: 12 # close issue | |||
act_user_id: 2 | |||
repo_id: 2 | |||
repo_id: 2 # private | |||
is_private: true | |||
created_unix: 1603228283 | |||
@@ -12,7 +12,7 @@ | |||
user_id: 3 | |||
op_type: 2 # rename repo | |||
act_user_id: 2 | |||
repo_id: 3 | |||
repo_id: 3 # private | |||
is_private: true | |||
content: oldRepoName | |||
@@ -21,7 +21,7 @@ | |||
user_id: 11 | |||
op_type: 1 # create repo | |||
act_user_id: 11 | |||
repo_id: 9 | |||
repo_id: 9 # public | |||
is_private: false | |||
- | |||
@@ -29,7 +29,7 @@ | |||
user_id: 16 | |||
op_type: 12 # close issue | |||
act_user_id: 16 | |||
repo_id: 22 | |||
repo_id: 22 # private | |||
is_private: true | |||
created_unix: 1603267920 | |||
@@ -37,7 +37,7 @@ | |||
user_id: 10 | |||
op_type: 1 # create repo | |||
act_user_id: 10 | |||
repo_id: 6 | |||
repo_id: 6 # private | |||
is_private: true | |||
created_unix: 1603010100 | |||
@@ -45,7 +45,7 @@ | |||
user_id: 10 | |||
op_type: 1 # create repo | |||
act_user_id: 10 | |||
repo_id: 7 | |||
repo_id: 7 # private | |||
is_private: true | |||
created_unix: 1603011300 | |||
@@ -53,6 +53,6 @@ | |||
user_id: 10 | |||
op_type: 1 # create repo | |||
act_user_id: 10 | |||
repo_id: 8 | |||
repo_id: 8 # public | |||
is_private: false | |||
created_unix: 1603011540 # grouped with id:7 |
@@ -9,6 +9,7 @@ import ( | |||
"testing" | |||
"time" | |||
"code.gitea.io/gitea/models/db" | |||
"code.gitea.io/gitea/models/unittest" | |||
user_model "code.gitea.io/gitea/models/user" | |||
"code.gitea.io/gitea/modules/json" | |||
@@ -72,7 +73,7 @@ func TestGetUserHeatmapDataByUser(t *testing.T) { | |||
} | |||
// get the action for comparison | |||
actions, err := GetFeeds(GetFeedsOptions{ | |||
actions, err := GetFeeds(db.DefaultContext, GetFeedsOptions{ | |||
RequestedUser: user, | |||
Actor: doer, | |||
IncludePrivate: true, |
@@ -34,10 +34,10 @@ func Auth(serviceName, userName, passwd string) (string, error) { | |||
if err = t.Authenticate(0); err != nil { | |||
return "", err | |||
} | |||
if err = t.AcctMgmt(0); err != nil { | |||
return "", err | |||
} | |||
return "", err | |||
} | |||
// PAM login names might suffer transformations in the PAM stack. | |||
// We should take whatever the PAM stack returns for it. |
@@ -418,6 +418,8 @@ func RepoAssignment(ctx *Context) (cancel context.CancelFunc) { | |||
userName := ctx.Params(":username") | |||
repoName := ctx.Params(":reponame") | |||
repoName = strings.TrimSuffix(repoName, ".git") | |||
repoName = strings.TrimSuffix(repoName, ".rss") | |||
repoName = strings.TrimSuffix(repoName, ".atom") | |||
// Check if the user is the same as the repository owner | |||
if ctx.IsSigned && ctx.User.LowerName == strings.ToLower(userName) { |
@@ -7,6 +7,7 @@ package feed | |||
import ( | |||
"fmt" | |||
"html" | |||
"net/http" | |||
"net/url" | |||
"strconv" | |||
"strings" | |||
@@ -66,7 +67,7 @@ func renderMarkdown(ctx *context.Context, act *models.Action, content string) st | |||
} | |||
// feedActionsToFeedItems convert gitea's Action feed to feeds Item | |||
func feedActionsToFeedItems(ctx *context.Context, actions []*models.Action) (items []*feeds.Item, err error) { | |||
func feedActionsToFeedItems(ctx *context.Context, actions models.ActionList) (items []*feeds.Item, err error) { | |||
for _, act := range actions { | |||
act.LoadActUser() | |||
@@ -247,3 +248,18 @@ func feedActionsToFeedItems(ctx *context.Context, actions []*models.Action) (ite | |||
} | |||
return | |||
} | |||
// GetFeedType return if it is a feed request and altered name and feed type. | |||
func GetFeedType(name string, req *http.Request) (bool, string, string) { | |||
if strings.HasSuffix(name, ".rss") || | |||
strings.Contains(req.Header.Get("Accept"), "application/rss+xml") { | |||
return true, strings.TrimSuffix(name, ".rss"), "rss" | |||
} | |||
if strings.HasSuffix(name, ".atom") || | |||
strings.Contains(req.Header.Get("Accept"), "application/atom+xml") { | |||
return true, strings.TrimSuffix(name, ".atom"), "atom" | |||
} | |||
return false, name, "" | |||
} |
@@ -15,48 +15,9 @@ import ( | |||
"github.com/gorilla/feeds" | |||
) | |||
// RetrieveFeeds loads feeds for the specified user | |||
func RetrieveFeeds(ctx *context.Context, options models.GetFeedsOptions) []*models.Action { | |||
actions, err := models.GetFeeds(options) | |||
if err != nil { | |||
ctx.ServerError("GetFeeds", err) | |||
return nil | |||
} | |||
// TODO: move load repoOwner of act.Repo into models.GetFeeds->loadAttributes() | |||
{ | |||
userCache := map[int64]*user_model.User{options.RequestedUser.ID: options.RequestedUser} | |||
if ctx.User != nil { | |||
userCache[ctx.User.ID] = ctx.User | |||
} | |||
for _, act := range actions { | |||
if act.ActUser != nil { | |||
userCache[act.ActUserID] = act.ActUser | |||
} | |||
} | |||
for _, act := range actions { | |||
repoOwner, ok := userCache[act.Repo.OwnerID] | |||
if !ok { | |||
repoOwner, err = user_model.GetUserByID(act.Repo.OwnerID) | |||
if err != nil { | |||
if user_model.IsErrUserNotExist(err) { | |||
continue | |||
} | |||
ctx.ServerError("GetUserByID", err) | |||
return nil | |||
} | |||
userCache[repoOwner.ID] = repoOwner | |||
} | |||
act.Repo.Owner = repoOwner | |||
} | |||
} | |||
return actions | |||
} | |||
// ShowUserFeed show user activity as RSS / Atom feed | |||
func ShowUserFeed(ctx *context.Context, ctxUser *user_model.User, formatType string) { | |||
actions := RetrieveFeeds(ctx, models.GetFeedsOptions{ | |||
actions, err := models.GetFeeds(ctx, models.GetFeedsOptions{ | |||
RequestedUser: ctxUser, | |||
Actor: ctx.User, | |||
IncludePrivate: false, | |||
@@ -64,7 +25,8 @@ func ShowUserFeed(ctx *context.Context, ctxUser *user_model.User, formatType str | |||
IncludeDeleted: false, | |||
Date: ctx.FormString("date"), | |||
}) | |||
if ctx.Written() { | |||
if err != nil { | |||
ctx.ServerError("GetFeeds", err) | |||
return | |||
} | |||
@@ -75,7 +37,6 @@ func ShowUserFeed(ctx *context.Context, ctxUser *user_model.User, formatType str | |||
Created: time.Now(), | |||
} | |||
var err error | |||
feed.Items, err = feedActionsToFeedItems(ctx, actions) | |||
if err != nil { | |||
ctx.ServerError("convert feed", err) |
@@ -0,0 +1,44 @@ | |||
// Copyright 2022 The Gitea Authors. All rights reserved. | |||
// Use of this source code is governed by a MIT-style | |||
// license that can be found in the LICENSE file. | |||
package feed | |||
import ( | |||
"time" | |||
"code.gitea.io/gitea/models" | |||
repo_model "code.gitea.io/gitea/models/repo" | |||
"code.gitea.io/gitea/modules/context" | |||
"github.com/gorilla/feeds" | |||
) | |||
// ShowRepoFeed shows user activity on the repo as RSS / Atom feed | |||
func ShowRepoFeed(ctx *context.Context, repo *repo_model.Repository, formatType string) { | |||
actions, err := models.GetFeeds(ctx, models.GetFeedsOptions{ | |||
RequestedRepo: repo, | |||
Actor: ctx.User, | |||
IncludePrivate: true, | |||
Date: ctx.FormString("date"), | |||
}) | |||
if err != nil { | |||
ctx.ServerError("GetFeeds", err) | |||
return | |||
} | |||
feed := &feeds.Feed{ | |||
Title: ctx.Tr("home.feed_of", repo.FullName()), | |||
Link: &feeds.Link{Href: repo.HTMLURL()}, | |||
Description: repo.Description, | |||
Created: time.Now(), | |||
} | |||
feed.Items, err = feedActionsToFeedItems(ctx, actions) | |||
if err != nil { | |||
ctx.ServerError("convert feed", err) | |||
return | |||
} | |||
writeFeed(ctx, feed, formatType) | |||
} |
@@ -38,6 +38,7 @@ import ( | |||
"code.gitea.io/gitea/modules/structs" | |||
"code.gitea.io/gitea/modules/typesniffer" | |||
"code.gitea.io/gitea/modules/util" | |||
"code.gitea.io/gitea/routers/web/feed" | |||
) | |||
const ( | |||
@@ -691,6 +692,14 @@ func checkHomeCodeViewable(ctx *context.Context) { | |||
// Home render repository home page | |||
func Home(ctx *context.Context) { | |||
isFeed, _, showFeedType := feed.GetFeedType(ctx.Params(":reponame"), ctx.Req) | |||
if isFeed { | |||
feed.ShowRepoFeed(ctx, ctx.Repo.Repository, showFeedType) | |||
return | |||
} | |||
ctx.Data["FeedURL"] = ctx.Repo.Repository.HTMLURL() | |||
checkHomeCodeViewable(ctx) | |||
if ctx.Written() { | |||
return |
@@ -29,7 +29,6 @@ import ( | |||
"code.gitea.io/gitea/modules/markup/markdown" | |||
"code.gitea.io/gitea/modules/setting" | |||
"code.gitea.io/gitea/modules/util" | |||
"code.gitea.io/gitea/routers/web/feed" | |||
issue_service "code.gitea.io/gitea/services/issue" | |||
pull_service "code.gitea.io/gitea/services/pull" | |||
@@ -131,7 +130,7 @@ func Dashboard(ctx *context.Context) { | |||
ctx.Data["MirrorCount"] = len(mirrors) | |||
ctx.Data["Mirrors"] = mirrors | |||
ctx.Data["Feeds"] = feed.RetrieveFeeds(ctx, models.GetFeedsOptions{ | |||
ctx.Data["Feeds"], err = models.GetFeeds(ctx, models.GetFeedsOptions{ | |||
RequestedUser: ctxUser, | |||
RequestedTeam: ctx.Org.Team, | |||
Actor: ctx.User, | |||
@@ -140,8 +139,8 @@ func Dashboard(ctx *context.Context) { | |||
IncludeDeleted: false, | |||
Date: ctx.FormString("date"), | |||
}) | |||
if ctx.Written() { | |||
if err != nil { | |||
ctx.ServerError("GetFeeds", err) | |||
return | |||
} | |||
@@ -74,19 +74,7 @@ func Profile(ctx *context.Context) { | |||
uname = strings.TrimSuffix(uname, ".gpg") | |||
} | |||
showFeedType := "" | |||
if strings.HasSuffix(uname, ".rss") { | |||
showFeedType = "rss" | |||
uname = strings.TrimSuffix(uname, ".rss") | |||
} else if strings.Contains(ctx.Req.Header.Get("Accept"), "application/rss+xml") { | |||
showFeedType = "rss" | |||
} | |||
if strings.HasSuffix(uname, ".atom") { | |||
showFeedType = "atom" | |||
uname = strings.TrimSuffix(uname, ".atom") | |||
} else if strings.Contains(ctx.Req.Header.Get("Accept"), "application/atom+xml") { | |||
showFeedType = "atom" | |||
} | |||
isShowFeed, uname, showFeedType := feed.GetFeedType(uname, ctx.Req) | |||
ctxUser := GetUserByName(ctx, uname) | |||
if ctx.Written() { | |||
@@ -95,7 +83,7 @@ func Profile(ctx *context.Context) { | |||
if ctxUser.IsOrganization() { | |||
// Show Org RSS feed | |||
if len(showFeedType) != 0 { | |||
if isShowFeed { | |||
feed.ShowUserFeed(ctx, ctxUser, showFeedType) | |||
return | |||
} | |||
@@ -123,11 +111,14 @@ func Profile(ctx *context.Context) { | |||
} | |||
// Show User RSS feed | |||
if len(showFeedType) != 0 { | |||
if isShowFeed { | |||
feed.ShowUserFeed(ctx, ctxUser, showFeedType) | |||
return | |||
} | |||
// advertise feed via meta tag | |||
ctx.Data["FeedURL"] = ctxUser.HTMLURL() | |||
// Show OpenID URIs | |||
openIDs, err := user_model.GetUserOpenIDs(ctxUser.ID) | |||
if err != nil { | |||
@@ -259,7 +250,7 @@ func Profile(ctx *context.Context) { | |||
total = ctxUser.NumFollowing | |||
case "activity": | |||
ctx.Data["Feeds"] = feed.RetrieveFeeds(ctx, models.GetFeedsOptions{ | |||
ctx.Data["Feeds"], err = models.GetFeeds(ctx, models.GetFeedsOptions{ | |||
RequestedUser: ctxUser, | |||
Actor: ctx.User, | |||
IncludePrivate: showPrivate, | |||
@@ -267,7 +258,8 @@ func Profile(ctx *context.Context) { | |||
IncludeDeleted: false, | |||
Date: ctx.FormString("date"), | |||
}) | |||
if ctx.Written() { | |||
if err != nil { | |||
ctx.ServerError("GetFeeds", err) | |||
return | |||
} | |||
case "stars": |
@@ -14,6 +14,10 @@ | |||
{{if .GoGetImport}} | |||
<meta name="go-import" content="{{.GoGetImport}} git {{.CloneLink.HTTPS}}"> | |||
<meta name="go-source" content="{{.GoGetImport}} _ {{.GoDocDirectory}} {{.GoDocFile}}"> | |||
{{end}} | |||
{{if .FeedURL}} | |||
<link rel="alternate" type="application/atom+xml" title="" href="{{.FeedURL}}.atom"> | |||
<link rel="alternate" type="application/rss+xml" title="" href="{{.FeedURL}}.rss"> | |||
{{end}} | |||
<script> | |||
<!-- /* eslint-disable */ --> |