From 369fe5696697cef33a188d9b985ac4b9824a4bdf Mon Sep 17 00:00:00 2001
From: KN4CK3R <admin@oldschoolhack.me>
Date: Mon, 19 Feb 2024 11:27:05 +0100
Subject: [PATCH] Show commit status for releases (#29149)

Fixes #29082

![grafik](https://github.com/go-gitea/gitea/assets/1666336/bb2ccde1-ee99-459d-9e74-0fb8ea79e8b3)

(cherry picked from commit 7e8ff709401d09467c3eee7c69cd9600d26a97a3)
---
 routers/web/repo/release.go         | 206 ++++++++++++++--------------
 services/actions/commit_status.go   |   3 +
 templates/repo/commit_statuses.tmpl |   4 +-
 templates/repo/release/list.tmpl    | 152 ++++++++++----------
 4 files changed, 184 insertions(+), 181 deletions(-)

diff --git a/routers/web/repo/release.go b/routers/web/repo/release.go
index 4d139f2b79..33e7f739ff 100644
--- a/routers/web/repo/release.go
+++ b/routers/web/repo/release.go
@@ -12,6 +12,7 @@ import (
 
 	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/models/db"
+	git_model "code.gitea.io/gitea/models/git"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unit"
 	user_model "code.gitea.io/gitea/models/user"
@@ -67,6 +68,88 @@ func calReleaseNumCommitsBehind(repoCtx *context.Repository, release *repo_model
 	return nil
 }
 
+type ReleaseInfo struct {
+	Release        *repo_model.Release
+	CommitStatus   *git_model.CommitStatus
+	CommitStatuses []*git_model.CommitStatus
+}
+
+func getReleaseInfos(ctx *context.Context, opts *repo_model.FindReleasesOptions) ([]*ReleaseInfo, error) {
+	releases, err := db.Find[repo_model.Release](ctx, opts)
+	if err != nil {
+		return nil, err
+	}
+
+	for _, release := range releases {
+		release.Repo = ctx.Repo.Repository
+	}
+
+	if err = repo_model.GetReleaseAttachments(ctx, releases...); err != nil {
+		return nil, err
+	}
+
+	// Temporary cache commits count of used branches to speed up.
+	countCache := make(map[string]int64)
+	cacheUsers := make(map[int64]*user_model.User)
+	if ctx.Doer != nil {
+		cacheUsers[ctx.Doer.ID] = ctx.Doer
+	}
+	var ok bool
+
+	canReadActions := ctx.Repo.CanRead(unit.TypeActions)
+
+	releaseInfos := make([]*ReleaseInfo, 0, len(releases))
+	for _, r := range releases {
+		if r.Publisher, ok = cacheUsers[r.PublisherID]; !ok {
+			r.Publisher, err = user_model.GetUserByID(ctx, r.PublisherID)
+			if err != nil {
+				if user_model.IsErrUserNotExist(err) {
+					r.Publisher = user_model.NewGhostUser()
+				} else {
+					return nil, err
+				}
+			}
+			cacheUsers[r.PublisherID] = r.Publisher
+		}
+
+		r.Note, err = markdown.RenderString(&markup.RenderContext{
+			Links: markup.Links{
+				Base: ctx.Repo.RepoLink,
+			},
+			Metas:   ctx.Repo.Repository.ComposeMetas(ctx),
+			GitRepo: ctx.Repo.GitRepo,
+			Ctx:     ctx,
+		}, r.Note)
+		if err != nil {
+			return nil, err
+		}
+
+		if !r.IsDraft {
+			if err := calReleaseNumCommitsBehind(ctx.Repo, r, countCache); err != nil {
+				return nil, err
+			}
+		}
+
+		info := &ReleaseInfo{
+			Release: r,
+		}
+
+		if canReadActions {
+			statuses, _, err := git_model.GetLatestCommitStatus(ctx, r.Repo.ID, r.Sha1, db.ListOptions{ListAll: true})
+			if err != nil {
+				return nil, err
+			}
+
+			info.CommitStatus = git_model.CalcCommitStatus(statuses)
+			info.CommitStatuses = statuses
+		}
+
+		releaseInfos = append(releaseInfos, info)
+	}
+
+	return releaseInfos, nil
+}
+
 // Releases render releases list page
 func Releases(ctx *context.Context) {
 	ctx.Data["PageIsReleaseList"] = true
@@ -91,77 +174,21 @@ func Releases(ctx *context.Context) {
 	writeAccess := ctx.Repo.CanWrite(unit.TypeReleases)
 	ctx.Data["CanCreateRelease"] = writeAccess && !ctx.Repo.Repository.IsArchived
 
-	opts := repo_model.FindReleasesOptions{
+	releases, err := getReleaseInfos(ctx, &repo_model.FindReleasesOptions{
 		ListOptions: listOptions,
 		// only show draft releases for users who can write, read-only users shouldn't see draft releases.
 		IncludeDrafts: writeAccess,
 		RepoID:        ctx.Repo.Repository.ID,
-	}
-
-	releases, err := db.Find[repo_model.Release](ctx, opts)
+	})
 	if err != nil {
-		ctx.ServerError("GetReleasesByRepoID", err)
+		ctx.ServerError("getReleaseInfos", err)
 		return
 	}
 
-	for _, release := range releases {
-		release.Repo = ctx.Repo.Repository
-	}
-
-	if err = repo_model.GetReleaseAttachments(ctx, releases...); err != nil {
-		ctx.ServerError("GetReleaseAttachments", err)
-		return
-	}
-
-	// Temporary cache commits count of used branches to speed up.
-	countCache := make(map[string]int64)
-	cacheUsers := make(map[int64]*user_model.User)
-	if ctx.Doer != nil {
-		cacheUsers[ctx.Doer.ID] = ctx.Doer
-	}
-	var ok bool
-
-	for _, r := range releases {
-		if r.Publisher, ok = cacheUsers[r.PublisherID]; !ok {
-			r.Publisher, err = user_model.GetUserByID(ctx, r.PublisherID)
-			if err != nil {
-				if user_model.IsErrUserNotExist(err) {
-					r.Publisher = user_model.NewGhostUser()
-				} else {
-					ctx.ServerError("GetUserByID", err)
-					return
-				}
-			}
-			cacheUsers[r.PublisherID] = r.Publisher
-		}
-
-		r.Note, err = markdown.RenderString(&markup.RenderContext{
-			Links: markup.Links{
-				Base: ctx.Repo.RepoLink,
-			},
-			Metas:   ctx.Repo.Repository.ComposeMetas(ctx),
-			GitRepo: ctx.Repo.GitRepo,
-			Ctx:     ctx,
-		}, r.Note)
-		if err != nil {
-			ctx.ServerError("RenderString", err)
-			return
-		}
-
-		if r.IsDraft {
-			continue
-		}
-
-		if err := calReleaseNumCommitsBehind(ctx.Repo, r, countCache); err != nil {
-			ctx.ServerError("calReleaseNumCommitsBehind", err)
-			return
-		}
-	}
-
 	ctx.Data["Releases"] = releases
 
 	numReleases := ctx.Data["NumReleases"].(int64)
-	pager := context.NewPagination(int(numReleases), opts.PageSize, opts.Page, 5)
+	pager := context.NewPagination(int(numReleases), listOptions.PageSize, listOptions.Page, 5)
 	pager.SetDefaultParams(ctx)
 	ctx.Data["Page"] = pager
 
@@ -249,15 +276,24 @@ func SingleRelease(ctx *context.Context) {
 	writeAccess := ctx.Repo.CanWrite(unit.TypeReleases)
 	ctx.Data["CanCreateRelease"] = writeAccess && !ctx.Repo.Repository.IsArchived
 
-	release, err := repo_model.GetRelease(ctx, ctx.Repo.Repository.ID, ctx.Params("*"))
+	releases, err := getReleaseInfos(ctx, &repo_model.FindReleasesOptions{
+		ListOptions: db.ListOptions{Page: 1, PageSize: 1},
+		RepoID:      ctx.Repo.Repository.ID,
+		TagNames:    []string{ctx.Params("*")},
+		// only show draft releases for users who can write, read-only users shouldn't see draft releases.
+		IncludeDrafts: writeAccess,
+	})
 	if err != nil {
-		if repo_model.IsErrReleaseNotExist(err) {
-			ctx.NotFound("GetRelease", err)
-			return
-		}
-		ctx.ServerError("GetReleasesByRepoID", err)
+		ctx.ServerError("getReleaseInfos", err)
 		return
 	}
+	if len(releases) != 1 {
+		ctx.NotFound("SingleRelease", err)
+		return
+	}
+
+	release := releases[0].Release
+
 	ctx.Data["PageIsSingleTag"] = release.IsTag
 	if release.IsTag {
 		ctx.Data["Title"] = release.TagName
@@ -265,43 +301,7 @@ func SingleRelease(ctx *context.Context) {
 		ctx.Data["Title"] = release.Title
 	}
 
-	release.Repo = ctx.Repo.Repository
-
-	err = repo_model.GetReleaseAttachments(ctx, release)
-	if err != nil {
-		ctx.ServerError("GetReleaseAttachments", err)
-		return
-	}
-
-	release.Publisher, err = user_model.GetUserByID(ctx, release.PublisherID)
-	if err != nil {
-		if user_model.IsErrUserNotExist(err) {
-			release.Publisher = user_model.NewGhostUser()
-		} else {
-			ctx.ServerError("GetUserByID", err)
-			return
-		}
-	}
-	if !release.IsDraft {
-		if err := calReleaseNumCommitsBehind(ctx.Repo, release, make(map[string]int64)); err != nil {
-			ctx.ServerError("calReleaseNumCommitsBehind", err)
-			return
-		}
-	}
-	release.Note, err = markdown.RenderString(&markup.RenderContext{
-		Links: markup.Links{
-			Base: ctx.Repo.RepoLink,
-		},
-		Metas:   ctx.Repo.Repository.ComposeMetas(ctx),
-		GitRepo: ctx.Repo.GitRepo,
-		Ctx:     ctx,
-	}, release.Note)
-	if err != nil {
-		ctx.ServerError("RenderString", err)
-		return
-	}
-
-	ctx.Data["Releases"] = []*repo_model.Release{release}
+	ctx.Data["Releases"] = releases
 	ctx.HTML(http.StatusOK, tplReleasesList)
 }
 
diff --git a/services/actions/commit_status.go b/services/actions/commit_status.go
index 72a3ab7ac6..edd1fd1568 100644
--- a/services/actions/commit_status.go
+++ b/services/actions/commit_status.go
@@ -64,6 +64,9 @@ func createCommitStatus(ctx context.Context, job *actions_model.ActionRunJob) er
 			return fmt.Errorf("head of pull request is missing in event payload")
 		}
 		sha = payload.PullRequest.Head.Sha
+	case webhook_module.HookEventRelease:
+		event = string(run.Event)
+		sha = run.CommitSHA
 	default:
 		return nil
 	}
diff --git a/templates/repo/commit_statuses.tmpl b/templates/repo/commit_statuses.tmpl
index ec2be6c38d..74c20a6a2c 100644
--- a/templates/repo/commit_statuses.tmpl
+++ b/templates/repo/commit_statuses.tmpl
@@ -1,10 +1,10 @@
 {{if .Statuses}}
 	{{if and (eq (len .Statuses) 1) .Status.TargetURL}}
-		<a class="gt-vm gt-no-underline" data-tippy="commit-statuses" href="{{.Status.TargetURL}}">
+		<a class="gt-vm {{.AdditionalClasses}} gt-no-underline" data-tippy="commit-statuses" href="{{.Status.TargetURL}}">
 			{{template "repo/commit_status" .Status}}
 		</a>
 	{{else}}
-		<span class="gt-vm" data-tippy="commit-statuses" tabindex="0">
+		<span class="gt-vm {{.AdditionalClasses}}" data-tippy="commit-statuses" tabindex="0">
 			{{template "repo/commit_status" .Status}}
 		</span>
 	{{end}}
diff --git a/templates/repo/release/list.tmpl b/templates/repo/release/list.tmpl
index fb2fce2950..6dbeb741db 100644
--- a/templates/repo/release/list.tmpl
+++ b/templates/repo/release/list.tmpl
@@ -5,90 +5,90 @@
 		{{template "base/alert" .}}
 		{{template "repo/release_tag_header" .}}
 		<ul id="release-list">
-			{{range $idx, $release := .Releases}}
+			{{range $idx, $info := .Releases}}
+				{{$release := $info.Release}}
 				<li class="ui grid">
 					<div class="ui four wide column meta">
-							<a class="muted" href="{{if not (and .Sha1 ($.Permission.CanRead $.UnitTypeCode))}}#{{else}}{{$.RepoLink}}/src/tag/{{.TagName | PathEscapeSegments}}{{end}}" rel="nofollow">{{svg "octicon-tag" 16 "gt-mr-2"}}{{.TagName}}</a>
-							{{if and .Sha1 ($.Permission.CanRead $.UnitTypeCode)}}
-								<a class="muted gt-mono" href="{{$.RepoLink}}/src/commit/{{.Sha1}}" rel="nofollow">{{svg "octicon-git-commit" 16 "gt-mr-2"}}{{ShortSha .Sha1}}</a>
-								{{template "repo/branch_dropdown" dict "root" $ "release" .}}
-							{{end}}
+						<a class="muted" href="{{if not (and $release.Sha1 ($.Permission.CanRead $.UnitTypeCode))}}#{{else}}{{$.RepoLink}}/src/tag/{{$release.TagName | PathEscapeSegments}}{{end}}" rel="nofollow">{{svg "octicon-tag" 16 "gt-mr-2"}}{{$release.TagName}}</a>
+						{{if and $release.Sha1 ($.Permission.CanRead $.UnitTypeCode)}}
+							<a class="muted gt-mono" href="{{$.RepoLink}}/src/commit/{{$release.Sha1}}" rel="nofollow">{{svg "octicon-git-commit" 16 "gt-mr-2"}}{{ShortSha $release.Sha1}}</a>
+							{{template "repo/branch_dropdown" dict "root" $ "release" $release}}
+						{{end}}
 					</div>
 					<div class="ui twelve wide column detail">
-							<div class="gt-df gt-ac gt-sb gt-fw gt-mb-3">
-								<h4 class="release-list-title gt-word-break">
-									<a href="{{$.RepoLink}}/releases/tag/{{.TagName | PathEscapeSegments}}">{{.Title}}</a>
-									{{if .IsDraft}}
-										<span class="ui yellow label">{{ctx.Locale.Tr "repo.release.draft"}}</span>
-									{{else if .IsPrerelease}}
-										<span class="ui orange label">{{ctx.Locale.Tr "repo.release.prerelease"}}</span>
-									{{else}}
-										<span class="ui green label">{{ctx.Locale.Tr "repo.release.stable"}}</span>
-									{{end}}
-								</h4>
-								<div>
-									{{if $.CanCreateRelease}}
-										<a class="muted" data-tooltip-content="{{ctx.Locale.Tr "repo.release.edit"}}" href="{{$.RepoLink}}/releases/edit/{{.TagName | PathEscapeSegments}}" rel="nofollow">
-											{{svg "octicon-pencil"}}
-										</a>
-									{{end}}
-								</div>
-							</div>
-							<p class="text grey">
-								<span class="author">
-								{{if .OriginalAuthor}}
-									{{svg (MigrationIcon .Repo.GetOriginalURLHostname) 20 "gt-mr-2"}}{{.OriginalAuthor}}
-								{{else if .Publisher}}
-									{{ctx.AvatarUtils.Avatar .Publisher 20 "gt-mr-2"}}
-									<a href="{{.Publisher.HomeLink}}">{{.Publisher.GetDisplayName}}</a>
+						<div class="gt-df gt-ac gt-sb gt-fw gt-mb-3">
+							<h4 class="release-list-title gt-word-break">
+								<a href="{{$.RepoLink}}/releases/tag/{{$release.TagName | PathEscapeSegments}}">{{$release.Title}}</a>
+								{{template "repo/commit_statuses" dict "Status" $info.CommitStatus "Statuses" $info.CommitStatuses "AdditionalClasses" "gt-df"}}
+								{{if $release.IsDraft}}
+									<span class="ui yellow label">{{ctx.Locale.Tr "repo.release.draft"}}</span>
+								{{else if $release.IsPrerelease}}
+									<span class="ui orange label">{{ctx.Locale.Tr "repo.release.prerelease"}}</span>
 								{{else}}
-									Ghost
+									<span class="ui green label">{{ctx.Locale.Tr "repo.release.stable"}}</span>
 								{{end}}
-								</span>
-								<span class="released">
-									{{ctx.Locale.Tr "repo.released_this"}}
-								</span>
-								{{if .CreatedUnix}}
-									<span class="time">{{TimeSinceUnix .CreatedUnix ctx.Locale}}</span>
+							</h4>
+							<div>
+								{{if $.CanCreateRelease}}
+									<a class="muted" data-tooltip-content="{{ctx.Locale.Tr "repo.release.edit"}}" href="{{$.RepoLink}}/releases/edit/{{$release.TagName | PathEscapeSegments}}" rel="nofollow">
+										{{svg "octicon-pencil"}}
+									</a>
 								{{end}}
-								{{if and (not .IsDraft) ($.Permission.CanRead $.UnitTypeCode)}}
-									| <span class="ahead"><a href="{{$.RepoLink}}/compare/{{.TagName | PathEscapeSegments}}...{{.TargetBehind | PathEscapeSegments}}">{{ctx.Locale.Tr "repo.release.ahead.commits" .NumCommitsBehind | Str2html}}</a> {{ctx.Locale.Tr "repo.release.ahead.target" .TargetBehind}}</span>
-								{{end}}
-							</p>
-							<div class="markup desc">
-								{{Str2html .Note}}
 							</div>
-							<div class="divider"></div>
-							<details class="download" {{if eq $idx 0}}open{{end}}>
-								<summary class="gt-my-4">
-									{{ctx.Locale.Tr "repo.release.downloads"}}
-								</summary>
-								<ul class="list">
-									{{if and (not $.DisableDownloadSourceArchives) (not .IsDraft) ($.Permission.CanRead $.UnitTypeCode)}}
-										<li>
-											<a class="archive-link" href="{{$.RepoLink}}/archive/{{.TagName | PathEscapeSegments}}.zip" rel="nofollow"><strong>{{svg "octicon-file-zip" 16 "gt-mr-2"}}{{ctx.Locale.Tr "repo.release.source_code"}} (ZIP)</strong></a>
-										</li>
-										<li>
-											<a class="archive-link" href="{{$.RepoLink}}/archive/{{.TagName | PathEscapeSegments}}.tar.gz" rel="nofollow"><strong>{{svg "octicon-file-zip" 16 "gt-mr-2"}}{{ctx.Locale.Tr "repo.release.source_code"}} (TAR.GZ)</strong></a>
-										</li>
-									{{end}}
-									{{if .Attachments}}
-										{{range .Attachments}}
-											<li>
-												<a target="_blank" rel="nofollow" href="{{.DownloadURL}}" download>
-													<strong>{{svg "octicon-package" 16 "gt-mr-2"}}{{.Name}}</strong>
-												</a>
-												<div>
-													<span class="text grey">{{.Size | FileSize}}</span>
-													<span data-tooltip-content="{{ctx.Locale.Tr "repo.release.download_count" (ctx.Locale.PrettyNumber .DownloadCount)}}">
-														{{svg "octicon-info"}}
-													</span>
-												</div>
-											</li>
-										{{end}}
-									{{end}}
-								</ul>
-							</details>
+						</div>
+						<p class="text grey">
+							<span class="author">
+							{{if $release.OriginalAuthor}}
+								{{svg (MigrationIcon $release.Repo.GetOriginalURLHostname) 20 "gt-mr-2"}}{{$release.OriginalAuthor}}
+							{{else if $release.Publisher}}
+								{{ctx.AvatarUtils.Avatar $release.Publisher 20 "gt-mr-2"}}
+								<a href="{{$release.Publisher.HomeLink}}">{{$release.Publisher.GetDisplayName}}</a>
+							{{else}}
+								Ghost
+							{{end}}
+							</span>
+							<span class="released">
+								{{ctx.Locale.Tr "repo.released_this"}}
+							</span>
+							{{if $release.CreatedUnix}}
+								<span class="time">{{TimeSinceUnix $release.CreatedUnix ctx.Locale}}</span>
+							{{end}}
+							{{if and (not $release.IsDraft) ($.Permission.CanRead $.UnitTypeCode)}}
+								| <span class="ahead"><a href="{{$.RepoLink}}/compare/{{$release.TagName | PathEscapeSegments}}...{{$release.TargetBehind | PathEscapeSegments}}">{{ctx.Locale.Tr "repo.release.ahead.commits" $release.NumCommitsBehind | Str2html}}</a> {{ctx.Locale.Tr "repo.release.ahead.target" $release.TargetBehind}}</span>
+							{{end}}
+						</p>
+						<div class="markup desc">
+							{{Str2html $release.Note}}
+						</div>
+						<div class="divider"></div>
+						<details class="download" {{if eq $idx 0}}open{{end}}>
+							<summary class="gt-my-4">
+								{{ctx.Locale.Tr "repo.release.downloads"}}
+							</summary>
+							<ul class="list">
+								{{if and (not $.DisableDownloadSourceArchives) (not $release.IsDraft) ($.Permission.CanRead $.UnitTypeCode)}}
+									<li>
+										<a class="archive-link" href="{{$.RepoLink}}/archive/{{$release.TagName | PathEscapeSegments}}.zip" rel="nofollow"><strong>{{svg "octicon-file-zip" 16 "gt-mr-2"}}{{ctx.Locale.Tr "repo.release.source_code"}} (ZIP)</strong></a>
+									</li>
+									<li>
+										<a class="archive-link" href="{{$.RepoLink}}/archive/{{$release.TagName | PathEscapeSegments}}.tar.gz" rel="nofollow"><strong>{{svg "octicon-file-zip" 16 "gt-mr-2"}}{{ctx.Locale.Tr "repo.release.source_code"}} (TAR.GZ)</strong></a>
+									</li>
+								{{end}}
+								{{range $release.Attachments}}
+									<li>
+										<a target="_blank" rel="nofollow" href="{{.DownloadURL}}" download>
+											<strong>{{svg "octicon-package" 16 "gt-mr-2"}}{{.Name}}</strong>
+										</a>
+										<div>
+											<span class="text grey">{{.Size | FileSize}}</span>
+											<span data-tooltip-content="{{ctx.Locale.Tr "repo.release.download_count" (ctx.Locale.PrettyNumber .DownloadCount)}}">
+												{{svg "octicon-info"}}
+											</span>
+										</div>
+									</li>
+								{{end}}
+							</ul>
+						</details>
 						<div class="dot"></div>
 					</div>
 				</li>