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(`.*`).ReplaceAllString(body, "") + body = regexp.MustCompile(`.*`).ReplaceAllString(body, "") + return body + } + t.Run("RSS feed", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + resp := MakeRequest(t, NewRequest(t, "GET", "/user2/repo1/releases.rss"), http.StatusOK) + assert.EqualValues(t, ` + + Releases for user2/repo1 + http://localhost/user2/repo1/release + + + + pre-release + http://localhost/user2/repo1/releases/tag/v1.0 + + some text for a pre release

+]]>
+ user2 + 5: http://localhost/user2/repo1/releases/tag/v1.0 + +
+ + testing-release + http://localhost/user2/repo1/releases/tag/v1.1 + + user2 + 1: http://localhost/user2/repo1/releases/tag/v1.1 + + +
+
`, normalize(resp.Body.String())) + }) + + t.Run("Atom feed", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + resp := MakeRequest(t, NewRequest(t, "GET", "/user2/repo1/releases.atom"), http.StatusOK) + assert.EqualValues(t, ` + Releases for user2/repo1 + http://localhost/user2/repo1/release + + + + pre-release + + 5: http://localhost/user2/repo1/releases/tag/v1.0 + <p dir="auto">some text for a pre release</p> + + + user2 + user2@noreply.example.org + + + + testing-release + + 1: http://localhost/user2/repo1/releases/tag/v1.1 + + + user2 + user2@noreply.example.org + + +`, normalize(resp.Body.String())) + }) +}