RSS/Atom support for Repos (#19055)

* support for repos
* refactor
* advertise the feeds via meta tags
* allow feed suffix and feed header
* optimize performance
This commit is contained in:
6543 2022-03-13 17:40:47 +01:00 committed by GitHub
parent 780cf76f6e
commit bc0d2c8ada
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 188 additions and 110 deletions

View file

@ -328,7 +328,7 @@ type GetFeedsOptions struct {
} }
// GetFeeds returns actions according to the provided options // 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 { if opts.RequestedUser == nil && opts.RequestedTeam == nil && opts.RequestedRepo == nil {
return nil, fmt.Errorf("need at least one of these filters: RequestedUser, RequestedTeam, RequestedRepo") 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 return nil, err
} }
sess := db.GetEngine(db.DefaultContext).Where(cond) e := db.GetEngine(ctx)
sess := e.Where(cond)
opts.SetDefaultValues() opts.SetDefaultValues()
sess = db.SetSessionPagination(sess, &opts) sess = db.SetSessionPagination(sess, &opts)
@ -349,7 +350,7 @@ func GetFeeds(opts GetFeedsOptions) ([]*Action, error) {
return nil, fmt.Errorf("Find: %v", err) 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) return nil, fmt.Errorf("LoadAttributes: %v", err)
} }

View file

@ -25,7 +25,7 @@ func (actions ActionList) getUserIDs() []int64 {
return keysInt64(userIDs) 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 { if len(actions) == 0 {
return nil, nil return nil, nil
} }
@ -42,12 +42,7 @@ func (actions ActionList) loadUsers(e db.Engine) ([]*user_model.User, error) {
for _, action := range actions { for _, action := range actions {
action.ActUser = userMaps[action.ActUserID] action.ActUser = userMaps[action.ActUserID]
} }
return valuesUser(userMaps), nil return userMaps, nil
}
// LoadUsers loads actions' all users
func (actions ActionList) LoadUsers() ([]*user_model.User, error) {
return actions.loadUsers(db.GetEngine(db.DefaultContext))
} }
func (actions ActionList) getRepoIDs() []int64 { func (actions ActionList) getRepoIDs() []int64 {
@ -60,45 +55,57 @@ func (actions ActionList) getRepoIDs() []int64 {
return keysInt64(repoIDs) 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 { if len(actions) == 0 {
return nil, nil return nil
} }
repoIDs := actions.getRepoIDs() repoIDs := actions.getRepoIDs()
repoMaps := make(map[int64]*repo_model.Repository, len(repoIDs)) repoMaps := make(map[int64]*repo_model.Repository, len(repoIDs))
err := e. err := e.In("id", repoIDs).Find(&repoMaps)
In("id", repoIDs).
Find(&repoMaps)
if err != nil { if err != nil {
return nil, fmt.Errorf("find repository: %v", err) return fmt.Errorf("find repository: %v", err)
} }
for _, action := range actions { for _, action := range actions {
action.Repo = repoMaps[action.RepoID] action.Repo = repoMaps[action.RepoID]
} }
return valuesRepository(repoMaps), nil return nil
} }
// LoadRepositories loads actions' all repositories func (actions ActionList) loadRepoOwner(e db.Engine, userMap map[int64]*user_model.User) (err error) {
func (actions ActionList) LoadRepositories() ([]*repo_model.Repository, error) { if userMap == nil {
return actions.loadRepositories(db.GetEngine(db.DefaultContext)) userMap = make(map[int64]*user_model.User)
}
// loadAttributes loads all attributes
func (actions ActionList) loadAttributes(e db.Engine) (err error) {
if _, err = actions.loadUsers(e); err != nil {
return
} }
if _, err = actions.loadRepositories(e); err != nil { for _, action := range actions {
return 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 return nil
} }
// LoadAttributes loads attributes of the actions // loadAttributes loads all attributes
func (actions ActionList) LoadAttributes() error { func (actions ActionList) loadAttributes(e db.Engine) error {
return actions.loadAttributes(db.GetEngine(db.DefaultContext)) userMap, err := actions.loadUsers(e)
if err != nil {
return err
}
if err := actions.loadRepositories(e); err != nil {
return err
}
return actions.loadRepoOwner(e, userMap)
} }

View file

@ -8,6 +8,7 @@ import (
"path" "path"
"testing" "testing"
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
@ -39,7 +40,7 @@ func TestGetFeeds(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase()) assert.NoError(t, unittest.PrepareTestDatabase())
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*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: user, RequestedUser: user,
Actor: user, Actor: user,
IncludePrivate: true, IncludePrivate: true,
@ -52,7 +53,7 @@ func TestGetFeeds(t *testing.T) {
assert.EqualValues(t, user.ID, actions[0].UserID) assert.EqualValues(t, user.ID, actions[0].UserID)
} }
actions, err = GetFeeds(GetFeedsOptions{ actions, err = GetFeeds(db.DefaultContext, GetFeedsOptions{
RequestedUser: user, RequestedUser: user,
Actor: user, Actor: user,
IncludePrivate: false, IncludePrivate: false,
@ -62,13 +63,54 @@ func TestGetFeeds(t *testing.T) {
assert.Len(t, actions, 0) 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) { func TestGetFeeds2(t *testing.T) {
// test with an organization user // test with an organization user
assert.NoError(t, unittest.PrepareTestDatabase()) assert.NoError(t, unittest.PrepareTestDatabase())
org := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3}).(*user_model.User) org := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3}).(*user_model.User)
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*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, RequestedUser: org,
Actor: user, Actor: user,
IncludePrivate: true, IncludePrivate: true,
@ -82,7 +124,7 @@ func TestGetFeeds2(t *testing.T) {
assert.EqualValues(t, org.ID, actions[0].UserID) assert.EqualValues(t, org.ID, actions[0].UserID)
} }
actions, err = GetFeeds(GetFeedsOptions{ actions, err = GetFeeds(db.DefaultContext, GetFeedsOptions{
RequestedUser: org, RequestedUser: org,
Actor: user, Actor: user,
IncludePrivate: false, IncludePrivate: false,

View file

@ -3,7 +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
repo_id: 2 repo_id: 2 # private
is_private: true is_private: true
created_unix: 1603228283 created_unix: 1603228283
@ -12,7 +12,7 @@
user_id: 3 user_id: 3
op_type: 2 # rename repo op_type: 2 # rename repo
act_user_id: 2 act_user_id: 2
repo_id: 3 repo_id: 3 # private
is_private: true is_private: true
content: oldRepoName content: oldRepoName
@ -21,7 +21,7 @@
user_id: 11 user_id: 11
op_type: 1 # create repo op_type: 1 # create repo
act_user_id: 11 act_user_id: 11
repo_id: 9 repo_id: 9 # public
is_private: false is_private: false
- -
@ -29,7 +29,7 @@
user_id: 16 user_id: 16
op_type: 12 # close issue op_type: 12 # close issue
act_user_id: 16 act_user_id: 16
repo_id: 22 repo_id: 22 # private
is_private: true is_private: true
created_unix: 1603267920 created_unix: 1603267920
@ -37,7 +37,7 @@
user_id: 10 user_id: 10
op_type: 1 # create repo op_type: 1 # create repo
act_user_id: 10 act_user_id: 10
repo_id: 6 repo_id: 6 # private
is_private: true is_private: true
created_unix: 1603010100 created_unix: 1603010100
@ -45,7 +45,7 @@
user_id: 10 user_id: 10
op_type: 1 # create repo op_type: 1 # create repo
act_user_id: 10 act_user_id: 10
repo_id: 7 repo_id: 7 # private
is_private: true is_private: true
created_unix: 1603011300 created_unix: 1603011300
@ -53,6 +53,6 @@
user_id: 10 user_id: 10
op_type: 1 # create repo op_type: 1 # create repo
act_user_id: 10 act_user_id: 10
repo_id: 8 repo_id: 8 # public
is_private: false is_private: false
created_unix: 1603011540 # grouped with id:7 created_unix: 1603011540 # grouped with id:7

View file

@ -9,6 +9,7 @@ import (
"testing" "testing"
"time" "time"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/json"
@ -72,7 +73,7 @@ func TestGetUserHeatmapDataByUser(t *testing.T) {
} }
// get the action for comparison // get the action for comparison
actions, err := GetFeeds(GetFeedsOptions{ actions, err := GetFeeds(db.DefaultContext, GetFeedsOptions{
RequestedUser: user, RequestedUser: user,
Actor: doer, Actor: doer,
IncludePrivate: true, IncludePrivate: true,

View file

@ -418,6 +418,8 @@ func RepoAssignment(ctx *Context) (cancel context.CancelFunc) {
userName := ctx.Params(":username") userName := ctx.Params(":username")
repoName := ctx.Params(":reponame") repoName := ctx.Params(":reponame")
repoName = strings.TrimSuffix(repoName, ".git") 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 // Check if the user is the same as the repository owner
if ctx.IsSigned && ctx.User.LowerName == strings.ToLower(userName) { if ctx.IsSigned && ctx.User.LowerName == strings.ToLower(userName) {

View file

@ -7,6 +7,7 @@ package feed
import ( import (
"fmt" "fmt"
"html" "html"
"net/http"
"net/url" "net/url"
"strconv" "strconv"
"strings" "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 // 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 { for _, act := range actions {
act.LoadActUser() act.LoadActUser()
@ -247,3 +248,18 @@ func feedActionsToFeedItems(ctx *context.Context, actions []*models.Action) (ite
} }
return 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, ""
}

View file

@ -15,48 +15,9 @@ import (
"github.com/gorilla/feeds" "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 // ShowUserFeed show user activity as RSS / Atom feed
func ShowUserFeed(ctx *context.Context, ctxUser *user_model.User, formatType string) { 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, RequestedUser: ctxUser,
Actor: ctx.User, Actor: ctx.User,
IncludePrivate: false, IncludePrivate: false,
@ -64,7 +25,8 @@ func ShowUserFeed(ctx *context.Context, ctxUser *user_model.User, formatType str
IncludeDeleted: false, IncludeDeleted: false,
Date: ctx.FormString("date"), Date: ctx.FormString("date"),
}) })
if ctx.Written() { if err != nil {
ctx.ServerError("GetFeeds", err)
return return
} }
@ -75,7 +37,6 @@ func ShowUserFeed(ctx *context.Context, ctxUser *user_model.User, formatType str
Created: time.Now(), Created: time.Now(),
} }
var err error
feed.Items, err = feedActionsToFeedItems(ctx, actions) feed.Items, err = feedActionsToFeedItems(ctx, actions)
if err != nil { if err != nil {
ctx.ServerError("convert feed", err) ctx.ServerError("convert feed", err)

44
routers/web/feed/repo.go Normal file
View file

@ -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)
}

View file

@ -38,6 +38,7 @@ import (
"code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/typesniffer" "code.gitea.io/gitea/modules/typesniffer"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/web/feed"
) )
const ( const (
@ -691,6 +692,14 @@ func checkHomeCodeViewable(ctx *context.Context) {
// Home render repository home page // Home render repository home page
func Home(ctx *context.Context) { 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) checkHomeCodeViewable(ctx)
if ctx.Written() { if ctx.Written() {
return return

View file

@ -29,7 +29,6 @@ import (
"code.gitea.io/gitea/modules/markup/markdown" "code.gitea.io/gitea/modules/markup/markdown"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/web/feed"
issue_service "code.gitea.io/gitea/services/issue" issue_service "code.gitea.io/gitea/services/issue"
pull_service "code.gitea.io/gitea/services/pull" pull_service "code.gitea.io/gitea/services/pull"
@ -131,7 +130,7 @@ func Dashboard(ctx *context.Context) {
ctx.Data["MirrorCount"] = len(mirrors) ctx.Data["MirrorCount"] = len(mirrors)
ctx.Data["Mirrors"] = mirrors ctx.Data["Mirrors"] = mirrors
ctx.Data["Feeds"] = feed.RetrieveFeeds(ctx, models.GetFeedsOptions{ ctx.Data["Feeds"], err = models.GetFeeds(ctx, models.GetFeedsOptions{
RequestedUser: ctxUser, RequestedUser: ctxUser,
RequestedTeam: ctx.Org.Team, RequestedTeam: ctx.Org.Team,
Actor: ctx.User, Actor: ctx.User,
@ -140,8 +139,8 @@ func Dashboard(ctx *context.Context) {
IncludeDeleted: false, IncludeDeleted: false,
Date: ctx.FormString("date"), Date: ctx.FormString("date"),
}) })
if err != nil {
if ctx.Written() { ctx.ServerError("GetFeeds", err)
return return
} }

View file

@ -74,19 +74,7 @@ func Profile(ctx *context.Context) {
uname = strings.TrimSuffix(uname, ".gpg") uname = strings.TrimSuffix(uname, ".gpg")
} }
showFeedType := "" isShowFeed, uname, showFeedType := feed.GetFeedType(uname, ctx.Req)
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"
}
ctxUser := GetUserByName(ctx, uname) ctxUser := GetUserByName(ctx, uname)
if ctx.Written() { if ctx.Written() {
@ -95,7 +83,7 @@ func Profile(ctx *context.Context) {
if ctxUser.IsOrganization() { if ctxUser.IsOrganization() {
// Show Org RSS feed // Show Org RSS feed
if len(showFeedType) != 0 { if isShowFeed {
feed.ShowUserFeed(ctx, ctxUser, showFeedType) feed.ShowUserFeed(ctx, ctxUser, showFeedType)
return return
} }
@ -123,11 +111,14 @@ func Profile(ctx *context.Context) {
} }
// Show User RSS feed // Show User RSS feed
if len(showFeedType) != 0 { if isShowFeed {
feed.ShowUserFeed(ctx, ctxUser, showFeedType) feed.ShowUserFeed(ctx, ctxUser, showFeedType)
return return
} }
// advertise feed via meta tag
ctx.Data["FeedURL"] = ctxUser.HTMLURL()
// Show OpenID URIs // Show OpenID URIs
openIDs, err := user_model.GetUserOpenIDs(ctxUser.ID) openIDs, err := user_model.GetUserOpenIDs(ctxUser.ID)
if err != nil { if err != nil {
@ -259,7 +250,7 @@ func Profile(ctx *context.Context) {
total = ctxUser.NumFollowing total = ctxUser.NumFollowing
case "activity": case "activity":
ctx.Data["Feeds"] = feed.RetrieveFeeds(ctx, models.GetFeedsOptions{ ctx.Data["Feeds"], err = models.GetFeeds(ctx, models.GetFeedsOptions{
RequestedUser: ctxUser, RequestedUser: ctxUser,
Actor: ctx.User, Actor: ctx.User,
IncludePrivate: showPrivate, IncludePrivate: showPrivate,
@ -267,7 +258,8 @@ func Profile(ctx *context.Context) {
IncludeDeleted: false, IncludeDeleted: false,
Date: ctx.FormString("date"), Date: ctx.FormString("date"),
}) })
if ctx.Written() { if err != nil {
ctx.ServerError("GetFeeds", err)
return return
} }
case "stars": case "stars":

View file

@ -14,6 +14,10 @@
{{if .GoGetImport}} {{if .GoGetImport}}
<meta name="go-import" content="{{.GoGetImport}} git {{.CloneLink.HTTPS}}"> <meta name="go-import" content="{{.GoGetImport}} git {{.CloneLink.HTTPS}}">
<meta name="go-source" content="{{.GoGetImport}} _ {{.GoDocDirectory}} {{.GoDocFile}}"> <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}} {{end}}
<script> <script>
<!-- /* eslint-disable */ --> <!-- /* eslint-disable */ -->