From 4f597b1866954ff94999a52af1a41fed06f0143e Mon Sep 17 00:00:00 2001
From: John Olheiser <john.olheiser@gmail.com>
Date: Sat, 18 Apr 2020 09:47:15 -0500
Subject: [PATCH] Add single release page and latest redirect (#11102)

* Add single release and latest release routes

Signed-off-by: jolheiser <john.olheiser@gmail.com>

* Update API and move latest search to models

Signed-off-by: jolheiser <john.olheiser@gmail.com>

* Fix swagger

Signed-off-by: jolheiser <john.olheiser@gmail.com>

Co-authored-by: guillep2k <18600385+guillep2k@users.noreply.github.com>
---
 models/release.go                | 28 +++++++++++++++
 modules/structs/release.go       |  1 +
 routers/repo/release.go          | 59 ++++++++++++++++++++++++++++++++
 routers/routes/routes.go         |  4 ++-
 templates/repo/release/list.tmpl |  2 +-
 templates/swagger/v1_json.tmpl   |  4 +++
 6 files changed, 96 insertions(+), 2 deletions(-)

diff --git a/models/release.go b/models/release.go
index 0f670f374f..0c76d17f4b 100644
--- a/models/release.go
+++ b/models/release.go
@@ -80,6 +80,11 @@ func (r *Release) TarURL() string {
 	return fmt.Sprintf("%s/archive/%s.tar.gz", r.Repo.HTMLURL(), r.TagName)
 }
 
+// HTMLURL the url for a release on the web UI. release must have attributes loaded
+func (r *Release) HTMLURL() string {
+	return fmt.Sprintf("%s/releases/tag/%s", r.Repo.HTMLURL(), r.TagName)
+}
+
 // APIFormat convert a Release to api.Release
 func (r *Release) APIFormat() *api.Release {
 	assets := make([]*api.Attachment, 0)
@@ -93,6 +98,7 @@ func (r *Release) APIFormat() *api.Release {
 		Title:        r.Title,
 		Note:         r.Note,
 		URL:          r.APIURL(),
+		HTMLURL:      r.HTMLURL(),
 		TarURL:       r.TarURL(),
 		ZipURL:       r.ZipURL(),
 		IsDraft:      r.IsDraft,
@@ -217,6 +223,28 @@ func GetReleasesByRepoID(repoID int64, opts FindReleasesOptions) ([]*Release, er
 	return rels, sess.Find(&rels)
 }
 
+// GetLatestReleaseByRepoID returns the latest release for a repository
+func GetLatestReleaseByRepoID(repoID int64) (*Release, error) {
+	cond := builder.NewCond().
+		And(builder.Eq{"repo_id": repoID}).
+		And(builder.Eq{"is_draft": false}).
+		And(builder.Eq{"is_prerelease": false}).
+		And(builder.Eq{"is_tag": false})
+
+	rel := new(Release)
+	has, err := x.
+		Desc("created_unix", "id").
+		Where(cond).
+		Get(rel)
+	if err != nil {
+		return nil, err
+	} else if !has {
+		return nil, ErrReleaseNotExist{0, "latest"}
+	}
+
+	return rel, nil
+}
+
 // GetReleasesByRepoIDAndNames returns a list of releases of repository according repoID and tagNames.
 func GetReleasesByRepoIDAndNames(ctx DBContext, repoID int64, tagNames []string) (rels []*Release, err error) {
 	err = ctx.e.
diff --git a/modules/structs/release.go b/modules/structs/release.go
index b7575af39a..38ce60bbe2 100644
--- a/modules/structs/release.go
+++ b/modules/structs/release.go
@@ -16,6 +16,7 @@ type Release struct {
 	Title        string `json:"name"`
 	Note         string `json:"body"`
 	URL          string `json:"url"`
+	HTMLURL      string `json:"html_url"`
 	TarURL       string `json:"tarball_url"`
 	ZipURL       string `json:"zipball_url"`
 	IsDraft      bool   `json:"draft"`
diff --git a/routers/repo/release.go b/routers/repo/release.go
index 5454195181..1eac3dce97 100644
--- a/routers/repo/release.go
+++ b/routers/repo/release.go
@@ -131,6 +131,65 @@ func Releases(ctx *context.Context) {
 	ctx.HTML(200, tplReleases)
 }
 
+// SingleRelease renders a single release's page
+func SingleRelease(ctx *context.Context) {
+	ctx.Data["Title"] = ctx.Tr("repo.release.releases")
+	ctx.Data["PageIsReleaseList"] = true
+
+	writeAccess := ctx.Repo.CanWrite(models.UnitTypeReleases)
+	ctx.Data["CanCreateRelease"] = writeAccess && !ctx.Repo.Repository.IsArchived
+
+	release, err := models.GetRelease(ctx.Repo.Repository.ID, ctx.Params("tag"))
+	if err != nil {
+		ctx.ServerError("GetReleasesByRepoID", err)
+		return
+	}
+
+	err = models.GetReleaseAttachments(release)
+	if err != nil {
+		ctx.ServerError("GetReleaseAttachments", err)
+		return
+	}
+
+	release.Publisher, err = models.GetUserByID(release.PublisherID)
+	if err != nil {
+		if models.IsErrUserNotExist(err) {
+			release.Publisher = models.NewGhostUser()
+		} else {
+			ctx.ServerError("GetUserByID", err)
+			return
+		}
+	}
+	if err := calReleaseNumCommitsBehind(ctx.Repo, release, make(map[string]int64)); err != nil {
+		ctx.ServerError("calReleaseNumCommitsBehind", err)
+		return
+	}
+	release.Note = markdown.RenderString(release.Note, ctx.Repo.RepoLink, ctx.Repo.Repository.ComposeMetas())
+
+	ctx.Data["Releases"] = []*models.Release{release}
+	ctx.HTML(200, tplReleases)
+}
+
+// LatestRelease redirects to the latest release
+func LatestRelease(ctx *context.Context) {
+	release, err := models.GetLatestReleaseByRepoID(ctx.Repo.Repository.ID)
+	if err != nil {
+		if models.IsErrReleaseNotExist(err) {
+			ctx.NotFound("LatestRelease", err)
+			return
+		}
+		ctx.ServerError("GetLatestReleaseByRepoID", err)
+		return
+	}
+
+	if err := release.LoadAttributes(); err != nil {
+		ctx.ServerError("LoadAttributes", err)
+		return
+	}
+
+	ctx.Redirect(release.HTMLURL())
+}
+
 // NewRelease render creating release page
 func NewRelease(ctx *context.Context) {
 	ctx.Data["Title"] = ctx.Tr("repo.release.new_release")
diff --git a/routers/routes/routes.go b/routers/routes/routes.go
index e2514054bf..a7828885bf 100644
--- a/routers/routes/routes.go
+++ b/routers/routes/routes.go
@@ -805,7 +805,9 @@ func RegisterRoutes(m *macaron.Macaron) {
 	// Releases
 	m.Group("/:username/:reponame", func() {
 		m.Group("/releases", func() {
-			m.Get("/", repo.MustBeNotEmpty, repo.Releases)
+			m.Get("/", repo.Releases)
+			m.Get("/tag/:tag", repo.SingleRelease)
+			m.Get("/latest", repo.LatestRelease)
 		}, repo.MustBeNotEmpty, context.RepoRef())
 		m.Group("/releases", func() {
 			m.Get("/new", repo.NewRelease)
diff --git a/templates/repo/release/list.tmpl b/templates/repo/release/list.tmpl
index 22aea09f7b..d0b160a1c0 100644
--- a/templates/repo/release/list.tmpl
+++ b/templates/repo/release/list.tmpl
@@ -49,7 +49,7 @@
 							</div>
 						{{else}}
 							<h3>
-								<a href="{{$.RepoLink}}/src/tag/{{.TagName | EscapePound}}">{{.Title}}</a>
+								<a href="{{$.RepoLink}}/releases/tag/{{.TagName | EscapePound}}">{{.Title}}</a>
 								{{if $.CanCreateRelease}}<small>(<a href="{{$.RepoLink}}/releases/edit/{{.TagName | EscapePound}}" rel="nofollow">{{$.i18n.Tr "repo.release.edit"}}</a>)</small>{{end}}
 							</h3>
 							<p class="text grey">
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl
index e9368a7d2a..e87af4f5c9 100644
--- a/templates/swagger/v1_json.tmpl
+++ b/templates/swagger/v1_json.tmpl
@@ -13107,6 +13107,10 @@
           "type": "boolean",
           "x-go-name": "IsDraft"
         },
+        "html_url": {
+          "type": "string",
+          "x-go-name": "HTMLURL"
+        },
         "id": {
           "type": "integer",
           "format": "int64",