From 56d4893b2a996da6388801c9c8ff16b9b588ad55 Mon Sep 17 00:00:00 2001
From: jladbrook <jhladbrook@gmail.com>
Date: Tue, 25 Apr 2023 15:08:29 +0100
Subject: [PATCH] Add RSS Feeds for branches and files (#22719)

Fix #22228 adding RSS feeds for branches and files.

RSS feeds are accessed through:

* [gitea]/src/branch/{branch}.rss
* [gitea]/src/branch/{branch}/{file_name}.rss

No changes have been made to the UI to expose the feed urls for branches
and files.
---
 routers/web/feed/branch.go                    | 50 +++++++++++++++++
 routers/web/feed/file.go                      | 56 +++++++++++++++++++
 routers/web/feed/render.go                    | 22 ++++++++
 routers/web/repo/branch.go                    |  6 ++
 routers/web/repo/view.go                      |  9 ++-
 routers/web/web.go                            |  3 +
 templates/repo/branch/list.tmpl               | 10 ++++
 templates/repo/home.tmpl                      | 10 ++--
 templates/repo/view_file.tmpl                 |  5 ++
 .../js/components/RepoBranchTagSelector.vue   |  3 +
 web_src/js/svg.js                             |  2 +
 11 files changed, 170 insertions(+), 6 deletions(-)
 create mode 100644 routers/web/feed/branch.go
 create mode 100644 routers/web/feed/file.go
 create mode 100644 routers/web/feed/render.go

diff --git a/routers/web/feed/branch.go b/routers/web/feed/branch.go
new file mode 100644
index 0000000000..fb9d2a7351
--- /dev/null
+++ b/routers/web/feed/branch.go
@@ -0,0 +1,50 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package feed
+
+import (
+	"fmt"
+	"strings"
+	"time"
+
+	"code.gitea.io/gitea/models/repo"
+	"code.gitea.io/gitea/modules/context"
+
+	"github.com/gorilla/feeds"
+)
+
+// ShowBranchFeed shows tags and/or releases on the repo as RSS / Atom feed
+func ShowBranchFeed(ctx *context.Context, repo *repo.Repository, formatType string) {
+	commits, err := ctx.Repo.Commit.CommitsByRange(0, 10)
+	if err != nil {
+		ctx.ServerError("ShowBranchFeed %s", err)
+		return
+	}
+
+	title := fmt.Sprintf("Latest commits for branch %s", ctx.Repo.BranchName)
+	link := &feeds.Link{Href: repo.HTMLURL() + "/" + ctx.Repo.BranchNameSubURL()}
+
+	feed := &feeds.Feed{
+		Title:       title,
+		Link:        link,
+		Description: repo.Description,
+		Created:     time.Now(),
+	}
+
+	for _, commit := range commits {
+		feed.Items = append(feed.Items, &feeds.Item{
+			Id:    commit.ID.String(),
+			Title: strings.TrimSpace(strings.Split(commit.Message(), "\n")[0]),
+			Link:  &feeds.Link{Href: repo.HTMLURL() + "/commit/" + commit.ID.String()},
+			Author: &feeds.Author{
+				Name:  commit.Author.Name,
+				Email: commit.Author.Email,
+			},
+			Description: commit.Message(),
+			Content:     commit.Message(),
+		})
+	}
+
+	writeFeed(ctx, feed, formatType)
+}
diff --git a/routers/web/feed/file.go b/routers/web/feed/file.go
new file mode 100644
index 0000000000..3dc9a4e27f
--- /dev/null
+++ b/routers/web/feed/file.go
@@ -0,0 +1,56 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package feed
+
+import (
+	"fmt"
+	"strings"
+	"time"
+
+	"code.gitea.io/gitea/models/repo"
+	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/modules/util"
+
+	"github.com/gorilla/feeds"
+)
+
+// ShowFileFeed shows tags and/or releases on the repo as RSS / Atom feed
+func ShowFileFeed(ctx *context.Context, repo *repo.Repository, formatType string) {
+	fileName := ctx.Repo.TreePath
+	if len(fileName) == 0 {
+		return
+	}
+	commits, err := ctx.Repo.GitRepo.CommitsByFileAndRange(ctx.Repo.RefName, fileName, 1)
+	if err != nil {
+		ctx.ServerError("ShowBranchFeed %s", err)
+		return
+	}
+
+	title := fmt.Sprintf("Latest commits for file %s", ctx.Repo.TreePath)
+
+	link := &feeds.Link{Href: repo.HTMLURL() + "/" + ctx.Repo.BranchNameSubURL() + "/" + util.PathEscapeSegments(ctx.Repo.TreePath)}
+
+	feed := &feeds.Feed{
+		Title:       title,
+		Link:        link,
+		Description: repo.Description,
+		Created:     time.Now(),
+	}
+
+	for _, commit := range commits {
+		feed.Items = append(feed.Items, &feeds.Item{
+			Id:    commit.ID.String(),
+			Title: strings.TrimSpace(strings.Split(commit.Message(), "\n")[0]),
+			Link:  &feeds.Link{Href: repo.HTMLURL() + "/commit/" + commit.ID.String()},
+			Author: &feeds.Author{
+				Name:  commit.Author.Name,
+				Email: commit.Author.Email,
+			},
+			Description: commit.Message(),
+			Content:     commit.Message(),
+		})
+	}
+
+	writeFeed(ctx, feed, formatType)
+}
diff --git a/routers/web/feed/render.go b/routers/web/feed/render.go
new file mode 100644
index 0000000000..0f327f87f2
--- /dev/null
+++ b/routers/web/feed/render.go
@@ -0,0 +1,22 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package feed
+
+import (
+	model "code.gitea.io/gitea/models/repo"
+	"code.gitea.io/gitea/modules/context"
+)
+
+// RenderBranchFeed render format for branch or file
+func RenderBranchFeed(ctx *context.Context) {
+	_, _, showFeedType := GetFeedType(ctx.Params(":reponame"), ctx.Req)
+	var renderer func(ctx *context.Context, repo *model.Repository, formatType string)
+	switch {
+	case ctx.Repo.TreePath == "":
+		renderer = ShowBranchFeed
+	case ctx.Repo.TreePath != "":
+		renderer = ShowFileFeed
+	}
+	renderer(ctx, ctx.Repo.Repository, showFeedType)
+}
diff --git a/routers/web/repo/branch.go b/routers/web/repo/branch.go
index 9f26634311..1014449f78 100644
--- a/routers/web/repo/branch.go
+++ b/routers/web/repo/branch.go
@@ -25,6 +25,7 @@ import (
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/utils"
+	"code.gitea.io/gitea/routers/web/feed"
 	"code.gitea.io/gitea/services/forms"
 	release_service "code.gitea.io/gitea/services/release"
 	repo_service "code.gitea.io/gitea/services/repository"
@@ -340,6 +341,11 @@ func getDeletedBranches(ctx *context.Context) ([]*Branch, error) {
 	return branches, nil
 }
 
+// BranchFeedRSS get feeds for tags in RSS format
+func BranchFeedRSS(ctx *context.Context) {
+	feed.ShowBranchFeed(ctx, ctx.Repo.Repository, "rss")
+}
+
 // CreateBranch creates new branch in repository
 func CreateBranch(ctx *context.Context) {
 	form := web.GetForm(ctx).(*forms.NewBranchForm)
diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go
index f9dca91844..9f2770a3ac 100644
--- a/routers/web/repo/view.go
+++ b/routers/web/repo/view.go
@@ -710,7 +710,14 @@ func Home(ctx *context.Context) {
 	if setting.Other.EnableFeed {
 		isFeed, _, showFeedType := feed.GetFeedType(ctx.Params(":reponame"), ctx.Req)
 		if isFeed {
-			feed.ShowRepoFeed(ctx, ctx.Repo.Repository, showFeedType)
+			switch {
+			case ctx.Link == fmt.Sprintf("%s.%s", ctx.Repo.RepoLink, showFeedType):
+				feed.ShowRepoFeed(ctx, ctx.Repo.Repository, showFeedType)
+			case ctx.Repo.TreePath == "":
+				feed.ShowBranchFeed(ctx, ctx.Repo.Repository, showFeedType)
+			case ctx.Repo.TreePath != "":
+				feed.ShowFileFeed(ctx, ctx.Repo.Repository, showFeedType)
+			}
 			return
 		}
 	}
diff --git a/routers/web/web.go b/routers/web/web.go
index 88b6777d73..f7f32734d8 100644
--- a/routers/web/web.go
+++ b/routers/web/web.go
@@ -1450,6 +1450,9 @@ func RegisterRoutes(m *web.Route) {
 			m.Get("/cherry-pick/{sha:([a-f0-9]{7,40})$}", repo.SetEditorconfigIfExists, repo.CherryPick)
 		}, repo.MustBeNotEmpty, context.RepoRef(), reqRepoCodeReader)
 
+		m.Get("/rss/branch/*", context.RepoRefByType(context.RepoRefBranch), feed.RenderBranchFeed)
+		m.Get("/atom/branch/*", context.RepoRefByType(context.RepoRefBranch), feed.RenderBranchFeed)
+
 		m.Group("/src", func() {
 			m.Get("/branch/*", context.RepoRefByType(context.RepoRefBranch), repo.Home)
 			m.Get("/tag/*", context.RepoRefByType(context.RepoRefTag), repo.Home)
diff --git a/templates/repo/branch/list.tmpl b/templates/repo/branch/list.tmpl
index b027c175a4..596d9ae78b 100644
--- a/templates/repo/branch/list.tmpl
+++ b/templates/repo/branch/list.tmpl
@@ -26,6 +26,11 @@
 										{{svg "octicon-git-branch"}}
 									</button>
 								{{end}}
+								{{if .EnableFeed}}
+									<a role="button" class="ui basic button icon" href="{{$.FeedURL}}/rss/branch/{{PathEscapeSegments .DefaultBranch}}">
+										{{svg "octicon-rss"}}
+									</a>
+								{{end}}
 								{{if not $.DisableDownloadSourceArchives}}
 									<button class="ui basic jump dropdown icon button" data-tooltip-content="{{$.locale.Tr "repo.branch.download" ($.DefaultBranch)}}">
 										{{svg "octicon-download"}}
@@ -113,6 +118,11 @@
 												{{svg "octicon-git-branch"}}
 											</button>
 										{{end}}
+										{{if $.EnableFeed}}
+											<a role="button" class="ui basic button icon" href="{{$.FeedURL}}/rss/branch/{{PathEscapeSegments .Name}}">
+												{{svg "octicon-rss"}}
+											</a>
+										{{end}}
 										{{if and (not .IsDeleted) (not $.DisableDownloadSourceArchives)}}
 											<button class="ui basic jump dropdown icon button" data-tooltip-content="{{$.locale.Tr "repo.branch.download" (.Name)}}">
 												{{svg "octicon-download"}}
diff --git a/templates/repo/home.tmpl b/templates/repo/home.tmpl
index 4db12f2c95..3f6cffbc8e 100644
--- a/templates/repo/home.tmpl
+++ b/templates/repo/home.tmpl
@@ -63,12 +63,12 @@
 				{{$n := len .TreeNames}}
 				{{$l := Eval $n "-" 1}}
 				<!-- If home page, show new pr. If not, show breadcrumb -->
+				{{if and (eq $n 0) .CanCompareOrPull .IsViewBranch (not .Repository.IsArchived)}}
+					<a href="{{CompareLink .BaseRepo .Repository .BranchName}}">
+						<button id="new-pull-request" class="ui compact basic button" data-tooltip-content="{{if .PullRequestCtx.Allowed}}{{.locale.Tr "repo.pulls.compare_changes"}}{{else}}{{.locale.Tr "action.compare_branch"}}{{end}}"><span class="text">{{svg "octicon-git-pull-request"}}</span></button>
+					</a>
+				{{end}}
 				{{if eq $n 0}}
-					{{if and .CanCompareOrPull .IsViewBranch (not .Repository.IsArchived)}}
-						<a href="{{CompareLink .BaseRepo .Repository .BranchName}}">
-							<button id="new-pull-request" class="ui compact basic button" data-tooltip-content="{{if .PullRequestCtx.Allowed}}{{.locale.Tr "repo.pulls.compare_changes"}}{{else}}{{.locale.Tr "action.compare_branch"}}{{end}}"><span class="text">{{svg "octicon-git-pull-request"}}</span></button>
-						</a>
-					{{end}}
 					<a href="{{.Repository.Link}}/find/{{.BranchNameSubURL}}" class="ui compact basic button">{{.locale.Tr "repo.find_file.go_to_file"}}</a>
 				{{end}}
 
diff --git a/templates/repo/view_file.tmpl b/templates/repo/view_file.tmpl
index 7ee08c9efc..fe67de5392 100644
--- a/templates/repo/view_file.tmpl
+++ b/templates/repo/view_file.tmpl
@@ -42,6 +42,11 @@
 				</div>
 				<a download href="{{$.RawFileLink}}"><span class="btn-octicon" data-tooltip-content="{{.locale.Tr "repo.download_file"}}">{{svg "octicon-download"}}</span></a>
 				<a id="copy-content" class="btn-octicon {{if not .CanCopyContent}} disabled{{end}}"{{if or .IsImageFile (and .HasSourceRenderedToggle (not .IsDisplayingSource))}} data-link="{{$.RawFileLink}}"{{end}} data-tooltip-content="{{if .CanCopyContent}}{{.locale.Tr "copy_content"}}{{else}}{{.locale.Tr "copy_type_unsupported"}}{{end}}">{{svg "octicon-copy" 14}}</a>
+				{{if .EnableFeed}}
+				<a class="btn-octicon" href="{{$.FeedURL}}/rss/{{$.BranchNameSubURL}}{{range $i, $v := .TreeNames}}/{{$v}}{{end}}">
+					{{svg "octicon-rss" 14}}
+				</a>
+				{{end}}
 				{{if .Repository.CanEnableEditor}}
 					{{if .CanEditFile}}
 						<a href="{{.RepoLink}}/_edit/{{PathEscapeSegments .BranchName}}/{{PathEscapeSegments .TreePath}}"><span class="btn-octicon" data-tooltip-content="{{.EditFileTooltip}}">{{svg "octicon-pencil"}}</span></a>
diff --git a/web_src/js/components/RepoBranchTagSelector.vue b/web_src/js/components/RepoBranchTagSelector.vue
index 6a65eeec6f..863da6206f 100644
--- a/web_src/js/components/RepoBranchTagSelector.vue
+++ b/web_src/js/components/RepoBranchTagSelector.vue
@@ -39,6 +39,9 @@
       <div class="scrolling menu" ref="scrollContainer">
         <div v-for="(item, index) in filteredItems" :key="item.name" class="item" :class="{selected: item.selected, active: active === index}" @click="selectItem(item)" :ref="'listItem' + index">
           {{ item.name }}
+          <a v-if="mode === 'branches'" role="button" class="ui compact muted right" :href="(branchURLPrefix + item.url).replace('src', 'rss')">
+            <svg-icon name="octicon-rss" :size="14"/>
+          </a>
         </div>
         <div class="item" v-if="showCreateNewBranch" :class="{active: active === filteredItems.length}" :ref="'listItem' + filteredItems.length">
           <a href="#" @click="createNewBranch()">
diff --git a/web_src/js/svg.js b/web_src/js/svg.js
index 6fa79b8e9b..94f858d7d7 100644
--- a/web_src/js/svg.js
+++ b/web_src/js/svg.js
@@ -43,6 +43,7 @@ import octiconChevronLeft from '../../public/img/svg/octicon-chevron-left.svg';
 import octiconOrganization from '../../public/img/svg/octicon-organization.svg';
 import octiconTag from '../../public/img/svg/octicon-tag.svg';
 import octiconGitBranch from '../../public/img/svg/octicon-git-branch.svg';
+import octiconRss from '../../public/img/svg/octicon-rss.svg';
 
 const svgs = {
   'octicon-blocked': octiconBlocked,
@@ -89,6 +90,7 @@ const svgs = {
   'octicon-organization': octiconOrganization,
   'octicon-tag': octiconTag,
   'octicon-git-branch': octiconGitBranch,
+  'octicon-rss': octiconRss,
 };
 
 // TODO: use a more general approach to access SVG icons.