forgejo/routers/web/repo/badges/badges.go
Gergely Nagy 2fc0d0b8a3
[GITEA] Add support for shields.io-based badges
Adds a new `/{username}/{repo}/badges` family of routes, which redirect
to various shields.io badges. The goal is to not reimplement badge
generation, and delegate it to shields.io (or a similar service), which
are already used by many. This way, we get all the goodies that come
with it: different styles, colors, logos, you name it.

So these routes are just thin wrappers around shields.io that make it
easier to display the information we want. The URL is configurable via
`app.ini`, and is templatable, allowing to use alternative badge
generator services with slightly different URL patterns.

Additionally, for compatibility with GitHub, there's an
`/{username}/{repo}/actions/workflows/{workflow_file}/badge.svg` route
that works much the same way as on GitHub. Change the hostname in the
URL, and done.

Fixes gitea#5633, gitea#23688, and also fixes #126.

Work sponsored by Codeberg e.V.

Signed-off-by: Gergely Nagy <forgejo@gergo.csillger.hu>
(cherry picked from commit fcd0f61212)
(cherry picked from commit 20d14f7844)
(cherry picked from commit 4359741431)
(cherry picked from commit 35cff45eb8)
2024-01-28 10:55:00 +01:00

165 lines
4.5 KiB
Go

// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package badges
import (
"fmt"
"net/url"
"strings"
actions_model "code.gitea.io/gitea/models/actions"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
context_module "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/setting"
)
func getBadgeURL(ctx *context_module.Context, label, text, color string) string {
sb := &strings.Builder{}
_ = setting.Badges.GeneratorURLTemplateTemplate.Execute(sb, map[string]string{
"label": url.PathEscape(label),
"text": url.PathEscape(text),
"color": url.PathEscape(color),
})
badgeURL := sb.String()
q := ctx.Req.URL.Query()
// Remove any `branch` or `event` query parameters. They're used by the
// workflow badge route, and do not need forwarding to the badge generator.
delete(q, "branch")
delete(q, "event")
if len(q) > 0 {
return fmt.Sprintf("%s?%s", badgeURL, q.Encode())
}
return badgeURL
}
func redirectToBadge(ctx *context_module.Context, label, text, color string) {
ctx.Redirect(getBadgeURL(ctx, label, text, color))
}
func errorBadge(ctx *context_module.Context, label, text string) {
ctx.Redirect(getBadgeURL(ctx, label, text, "crimson"))
}
func GetWorkflowBadge(ctx *context_module.Context) {
branch := ctx.Req.URL.Query().Get("branch")
if branch == "" {
branch = ctx.Repo.Repository.DefaultBranch
}
branch = fmt.Sprintf("refs/heads/%s", branch)
event := ctx.Req.URL.Query().Get("event")
workflowFile := ctx.Params("workflow_name")
run, err := actions_model.GetLatestRunForBranchAndWorkflow(ctx, ctx.Repo.Repository.ID, branch, workflowFile, event)
if err != nil {
errorBadge(ctx, workflowFile, "Not found")
return
}
var color string
switch run.Status {
case actions_model.StatusUnknown:
color = "lightgrey"
case actions_model.StatusWaiting:
color = "lightgrey"
case actions_model.StatusRunning:
color = "gold"
case actions_model.StatusSuccess:
color = "brightgreen"
case actions_model.StatusFailure:
color = "crimson"
case actions_model.StatusCancelled:
color = "orange"
case actions_model.StatusSkipped:
color = "blue"
case actions_model.StatusBlocked:
color = "yellow"
default:
color = "lightgrey"
}
redirectToBadge(ctx, workflowFile, run.Status.String(), color)
}
func getIssueOrPullBadge(ctx *context_module.Context, label, variant string, num int) {
var text string
if len(variant) > 0 {
text = fmt.Sprintf("%d %s", num, variant)
} else {
text = fmt.Sprintf("%d", num)
}
redirectToBadge(ctx, label, text, "blue")
}
func getIssueBadge(ctx *context_module.Context, variant string, num int) {
if !ctx.Repo.CanRead(unit.TypeIssues) &&
!ctx.Repo.CanRead(unit.TypeExternalTracker) {
errorBadge(ctx, "issues", "Not found")
return
}
_, err := ctx.Repo.Repository.GetUnit(ctx, unit.TypeExternalTracker)
if err == nil {
errorBadge(ctx, "issues", "Not found")
return
}
getIssueOrPullBadge(ctx, "issues", variant, num)
}
func getPullBadge(ctx *context_module.Context, variant string, num int) {
if !ctx.Repo.Repository.CanEnablePulls() || !ctx.Repo.CanRead(unit.TypePullRequests) {
errorBadge(ctx, "pulls", "Not found")
return
}
getIssueOrPullBadge(ctx, "pulls", variant, num)
}
func GetOpenIssuesBadge(ctx *context_module.Context) {
getIssueBadge(ctx, "open", ctx.Repo.Repository.NumOpenIssues)
}
func GetClosedIssuesBadge(ctx *context_module.Context) {
getIssueBadge(ctx, "closed", ctx.Repo.Repository.NumClosedIssues)
}
func GetTotalIssuesBadge(ctx *context_module.Context) {
getIssueBadge(ctx, "", ctx.Repo.Repository.NumIssues)
}
func GetOpenPullsBadge(ctx *context_module.Context) {
getPullBadge(ctx, "open", ctx.Repo.Repository.NumOpenPulls)
}
func GetClosedPullsBadge(ctx *context_module.Context) {
getPullBadge(ctx, "closed", ctx.Repo.Repository.NumClosedPulls)
}
func GetTotalPullsBadge(ctx *context_module.Context) {
getPullBadge(ctx, "", ctx.Repo.Repository.NumPulls)
}
func GetStarsBadge(ctx *context_module.Context) {
redirectToBadge(ctx, "stars", fmt.Sprintf("%d", ctx.Repo.Repository.NumStars), "blue")
}
func GetLatestReleaseBadge(ctx *context_module.Context) {
release, err := repo_model.GetLatestReleaseByRepoID(ctx, ctx.Repo.Repository.ID)
if err != nil {
if repo_model.IsErrReleaseNotExist(err) {
errorBadge(ctx, "release", "Not found")
return
}
ctx.ServerError("GetLatestReleaseByRepoID", err)
}
if err := release.LoadAttributes(ctx); err != nil {
ctx.ServerError("LoadAttributes", err)
return
}
redirectToBadge(ctx, "release", release.TagName, "blue")
}