diff --git a/models/repo/release_list.go b/models/repo/release_list.go
new file mode 100644
index 0000000000..6c33262125
--- /dev/null
+++ b/models/repo/release_list.go
@@ -0,0 +1,45 @@
+// Copyright 2025 The Forgejo Authors. All rights reserved.
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package repo
+
+import (
+ "context"
+
+ user_model "code.gitea.io/gitea/models/user"
+)
+
+type ReleaseList []*Release
+
+// LoadAttributes loads the repository and publisher for the releases.
+func (r ReleaseList) LoadAttributes(ctx context.Context) error {
+ repoCache := make(map[int64]*Repository)
+ userCache := make(map[int64]*user_model.User)
+
+ for _, release := range r {
+ var err error
+ repo, ok := repoCache[release.RepoID]
+ if !ok {
+ repo, err = GetRepositoryByID(ctx, release.RepoID)
+ if err != nil {
+ return err
+ }
+ repoCache[release.RepoID] = repo
+ }
+ release.Repo = repo
+
+ publisher, ok := userCache[release.PublisherID]
+ if !ok {
+ publisher, err = user_model.GetUserByID(ctx, release.PublisherID)
+ if err != nil {
+ if !user_model.IsErrUserNotExist(err) {
+ return err
+ }
+ publisher = user_model.NewGhostUser()
+ }
+ userCache[release.PublisherID] = publisher
+ }
+ release.Publisher = publisher
+ }
+ return nil
+}
diff --git a/models/repo/release_list_test.go b/models/repo/release_list_test.go
new file mode 100644
index 0000000000..cbd77683d0
--- /dev/null
+++ b/models/repo/release_list_test.go
@@ -0,0 +1,42 @@
+// Copyright 2025 The Forgejo Authors. All rights reserved.
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package repo
+
+import (
+ "testing"
+
+ "code.gitea.io/gitea/models/unittest"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestReleaseListLoadAttributes(t *testing.T) {
+ require.NoError(t, unittest.PrepareTestDatabase())
+
+ releases := ReleaseList{&Release{
+ RepoID: 1,
+ PublisherID: 1,
+ }, &Release{
+ RepoID: 2,
+ PublisherID: 2,
+ }, &Release{
+ RepoID: 1,
+ PublisherID: 2,
+ }, &Release{
+ RepoID: 2,
+ PublisherID: 1,
+ }}
+
+ require.NoError(t, releases.LoadAttributes(t.Context()))
+
+ assert.EqualValues(t, 1, releases[0].Repo.ID)
+ assert.EqualValues(t, 1, releases[0].Publisher.ID)
+ assert.EqualValues(t, 2, releases[1].Repo.ID)
+ assert.EqualValues(t, 2, releases[1].Publisher.ID)
+ assert.EqualValues(t, 1, releases[2].Repo.ID)
+ assert.EqualValues(t, 2, releases[2].Publisher.ID)
+ assert.EqualValues(t, 2, releases[3].Repo.ID)
+ assert.EqualValues(t, 1, releases[3].Publisher.ID)
+}
diff --git a/routers/web/feed/convert.go b/routers/web/feed/convert.go
index d3d01a87e0..5f7687d803 100644
--- a/routers/web/feed/convert.go
+++ b/routers/web/feed/convert.go
@@ -298,14 +298,14 @@ func GetFeedType(name string, req *http.Request) (bool, string, string) {
return false, name, ""
}
-// feedActionsToFeedItems convert gitea's Repo's Releases to feeds Item
-func releasesToFeedItems(ctx *context.Context, releases []*repo_model.Release) (items []*feeds.Item, err error) {
- for _, rel := range releases {
- err := rel.LoadAttributes(ctx)
- if err != nil {
- return nil, err
- }
+// feedActionsToFeedItems convert repository releases into feed items.
+func releasesToFeedItems(ctx *context.Context, releases repo_model.ReleaseList) (items []*feeds.Item, err error) {
+ if err := releases.LoadAttributes(ctx); err != nil {
+ return nil, err
+ }
+ composeCache := make(map[int64]map[string]string)
+ for _, rel := range releases {
var title string
var content template.HTML
@@ -315,13 +315,19 @@ func releasesToFeedItems(ctx *context.Context, releases []*repo_model.Release) (
title = rel.Title
}
+ metas, ok := composeCache[rel.RepoID]
+ if !ok {
+ metas = rel.Repo.ComposeMetas(ctx)
+ composeCache[rel.RepoID] = metas
+ }
+
link := &feeds.Link{Href: rel.HTMLURL()}
content, err = markdown.RenderString(&markup.RenderContext{
Ctx: ctx,
Links: markup.Links{
Base: rel.Repo.Link(),
},
- Metas: rel.Repo.ComposeMetas(ctx),
+ Metas: metas,
}, rel.Note)
if err != nil {
return nil, err
diff --git a/tests/integration/release_feed_test.go b/tests/integration/release_feed_test.go
new file mode 100644
index 0000000000..fd1f42c3cd
--- /dev/null
+++ b/tests/integration/release_feed_test.go
@@ -0,0 +1,91 @@
+// Copyright 2025 The Forgejo Authors. All rights reserved.
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package integration
+
+import (
+ "net/http"
+ "regexp"
+ "testing"
+
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestReleaseFeed(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ normalize := func(body string) string {
+ // Remove port.
+ body = regexp.MustCompile(`localhost:\d+`).ReplaceAllString(body, "localhost")
+ // date is timezone dependent.
+ body = regexp.MustCompile(`