From fddb66da3ae01779577074717dc061b7cbf1fab9 Mon Sep 17 00:00:00 2001
From: bytedream <bytedream@protonmail.com>
Date: Sat, 29 Mar 2025 11:34:54 +0100
Subject: [PATCH 1/7] Add badge flat-square style

---
 modules/badge/badge.go                          |  9 +++++++++
 routers/web/repo/actions/badge.go               | 17 ++++++++++++++---
 templates/devtest/badge-actions-svg.tmpl        | 13 ++++++++++---
 .../actions/runner_badge_flat-square.tmpl       | 15 +++++++++++++++
 ...runner_badge.tmpl => runner_badge_flat.tmpl} |  0
 5 files changed, 48 insertions(+), 6 deletions(-)
 create mode 100644 templates/shared/actions/runner_badge_flat-square.tmpl
 rename templates/shared/actions/{runner_badge.tmpl => runner_badge_flat.tmpl} (100%)

diff --git a/modules/badge/badge.go b/modules/badge/badge.go
index fdf9866f607a5..d2fd2fdec63c8 100644
--- a/modules/badge/badge.go
+++ b/modules/badge/badge.go
@@ -49,11 +49,20 @@ func (b Badge) Width() int {
 	return b.Label.width + b.Message.width
 }
 
+// Style represents the style of a badge
+type Style string
+
+const (
+	StyleFlat       = "flat"
+	StyleFlatSquare = "flat-square"
+)
+
 const (
 	defaultOffset     = 10
 	defaultFontSize   = 11
 	DefaultColor      = "#9f9f9f" // Grey
 	DefaultFontFamily = "DejaVu Sans,Verdana,Geneva,sans-serif"
+	DefaultStyle      = StyleFlat
 )
 
 var StatusColorMap = map[actions_model.Status]string{
diff --git a/routers/web/repo/actions/badge.go b/routers/web/repo/actions/badge.go
index e920ecaf58063..1dd57a0b015d7 100644
--- a/routers/web/repo/actions/badge.go
+++ b/routers/web/repo/actions/badge.go
@@ -8,6 +8,7 @@ import (
 	"fmt"
 	"net/http"
 	"path/filepath"
+	"slices"
 	"strings"
 
 	actions_model "code.gitea.io/gitea/models/actions"
@@ -25,15 +26,25 @@ func GetWorkflowBadge(ctx *context.Context) {
 	branchRef := fmt.Sprintf("refs/heads/%s", branch)
 	event := ctx.Req.URL.Query().Get("event")
 
-	badge, err := getWorkflowBadge(ctx, workflowFile, branchRef, event)
+	style := ctx.Req.URL.Query().Get("style")
+	if !slices.Contains([]badge.Style{badge.StyleFlat, badge.StyleFlatSquare}, badge.Style(style)) {
+		style = badge.DefaultStyle
+	}
+
+	b, err := getWorkflowBadge(ctx, workflowFile, branchRef, event)
 	if err != nil {
 		ctx.ServerError("GetWorkflowBadge", err)
 		return
 	}
 
-	ctx.Data["Badge"] = badge
+	ctx.Data["Badge"] = b
 	ctx.RespHeader().Set("Content-Type", "image/svg+xml")
-	ctx.HTML(http.StatusOK, "shared/actions/runner_badge")
+	switch ctx.Req.URL.Query().Get("style") {
+	case badge.StyleFlat:
+		ctx.HTML(http.StatusOK, "shared/actions/runner_badge_flat")
+	case badge.StyleFlatSquare:
+		ctx.HTML(http.StatusOK, "shared/actions/runner_badge_flat-square")
+	}
 }
 
 func getWorkflowBadge(ctx *context.Context, workflowFile, branchName, event string) (badge.Badge, error) {
diff --git a/templates/devtest/badge-actions-svg.tmpl b/templates/devtest/badge-actions-svg.tmpl
index 8125793bb3fa4..5be4fb313128d 100644
--- a/templates/devtest/badge-actions-svg.tmpl
+++ b/templates/devtest/badge-actions-svg.tmpl
@@ -3,9 +3,16 @@
 	<div>
 		<h1>Actions SVG</h1>
 		<form class="tw-my-3">
-			{{range $fontName := .BadgeFontFamilyNames}}
-				<label><input name="font" type="radio" value="{{$fontName}}" {{Iif (eq $.SelectedFontFamilyName $fontName) "checked"}}>{{$fontName}}</label>
-			{{end}}
+			<div class="tw-mb-2">
+				{{range $fontName := .BadgeFontFamilyNames}}
+					<label><input name="font" type="radio" value="{{$fontName}}" {{Iif (eq $.SelectedFontFamilyName $fontName) "checked"}}>{{$fontName}}</label>
+				{{end}}
+			</div>
+			<div class="tw-mb-2">
+				{{range $style := .BadgeStyles}}
+					<label><input name="style" type="radio" value="{{$style}}" {{Iif (eq $.SelectedStyle $style) "checked"}}>{{$style}}</label>
+				{{end}}
+			</div>
 			<button>submit</button>
 		</form>
 		<div class="flex-text-block tw-flex-wrap">
diff --git a/templates/shared/actions/runner_badge_flat-square.tmpl b/templates/shared/actions/runner_badge_flat-square.tmpl
new file mode 100644
index 0000000000000..cc1dc1e8f3a90
--- /dev/null
+++ b/templates/shared/actions/runner_badge_flat-square.tmpl
@@ -0,0 +1,15 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="{{.Badge.Width}}" height="20"
+	role="img" aria-label="{{.Badge.Label.Text}}: {{.Badge.Message.Text}}">
+	<title>{{.Badge.Label.Text}}: {{.Badge.Message.Text}}</title>
+	<g shape-rendering="crispEdges">
+		<rect width="{{.Badge.Label.Width}}" height="20" fill="#555" />
+		<rect x="{{.Badge.Label.Width}}" width="{{.Badge.Message.Width}}" height="20" fill="{{.Badge.Color}}" />
+	</g>
+	<g fill="#fff" text-anchor="middle" font-family="{{.Badge.FontFamily}}"
+		text-rendering="geometricPrecision" font-size="{{.Badge.FontSize}}">
+		<text x="{{.Badge.Label.X}}" y="140"
+			transform="scale(.1)" fill="#fff" textLength="{{.Badge.Label.TextLength}}">{{.Badge.Label.Text}}</text>
+		<text x="{{.Badge.Message.X}}" y="140" transform="scale(.1)" fill="#fff"
+			textLength="{{.Badge.Message.TextLength}}">{{.Badge.Message.Text}}</text>
+	</g>
+</svg>
diff --git a/templates/shared/actions/runner_badge.tmpl b/templates/shared/actions/runner_badge_flat.tmpl
similarity index 100%
rename from templates/shared/actions/runner_badge.tmpl
rename to templates/shared/actions/runner_badge_flat.tmpl

From 8a210af901dc9d64197217e46a0c900c81432679 Mon Sep 17 00:00:00 2001
From: bytedream <bytedream@protonmail.com>
Date: Sat, 29 Mar 2025 11:35:06 +0100
Subject: [PATCH 2/7] Add devtest for new style

---
 routers/web/devtest/devtest.go | 16 +++++++++++++++-
 1 file changed, 15 insertions(+), 1 deletion(-)

diff --git a/routers/web/devtest/devtest.go b/routers/web/devtest/devtest.go
index 063ff42409fa9..6c01b37bd7717 100644
--- a/routers/web/devtest/devtest.go
+++ b/routers/web/devtest/devtest.go
@@ -4,6 +4,7 @@
 package devtest
 
 import (
+	"fmt"
 	"html/template"
 	"net/http"
 	"path"
@@ -127,7 +128,9 @@ func prepareMockDataBadgeCommitSign(ctx *context.Context) {
 
 func prepareMockDataBadgeActionsSvg(ctx *context.Context) {
 	fontFamilyNames := strings.Split(badge.DefaultFontFamily, ",")
+	styles := []badge.Style{badge.StyleFlat, badge.StyleFlatSquare}
 	selectedFontFamilyName := ctx.FormString("font", fontFamilyNames[0])
+	selectedStyle := ctx.FormString("style", badge.DefaultStyle)
 	var badges []badge.Badge
 	badges = append(badges, badge.GenerateBadge("啊啊啊啊啊啊啊啊啊啊啊啊", "🌞🌞🌞🌞🌞", "green"))
 	for r := rune(0); r < 256; r++ {
@@ -141,7 +144,16 @@ func prepareMockDataBadgeActionsSvg(ctx *context.Context) {
 	for i, b := range badges {
 		b.IDPrefix = "devtest-" + strconv.FormatInt(int64(i), 10) + "-"
 		b.FontFamily = selectedFontFamilyName
-		h, err := ctx.RenderToHTML("shared/actions/runner_badge", map[string]any{"Badge": b})
+		var h template.HTML
+		var err error
+		switch selectedStyle {
+		case badge.StyleFlat:
+			h, err = ctx.RenderToHTML("shared/actions/runner_badge_flat", map[string]any{"Badge": b})
+		case badge.StyleFlatSquare:
+			h, err = ctx.RenderToHTML("shared/actions/runner_badge_flat-square", map[string]any{"Badge": b})
+		default:
+			err = fmt.Errorf("unknown badge style: %s", selectedStyle)
+		}
 		if err != nil {
 			ctx.ServerError("RenderToHTML", err)
 			return
@@ -151,6 +163,8 @@ func prepareMockDataBadgeActionsSvg(ctx *context.Context) {
 	ctx.Data["BadgeSVGs"] = badgeSVGs
 	ctx.Data["BadgeFontFamilyNames"] = fontFamilyNames
 	ctx.Data["SelectedFontFamilyName"] = selectedFontFamilyName
+	ctx.Data["BadgeStyles"] = styles
+	ctx.Data["SelectedStyle"] = selectedStyle
 }
 
 func prepareMockData(ctx *context.Context) {

From ad7e126966784c0cf596420f97ae5f78021109ce Mon Sep 17 00:00:00 2001
From: bytedream <bytedream@protonmail.com>
Date: Sat, 29 Mar 2025 11:45:39 +0100
Subject: [PATCH 3/7] Fix endpoint style switch

---
 routers/web/repo/actions/badge.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/routers/web/repo/actions/badge.go b/routers/web/repo/actions/badge.go
index 1dd57a0b015d7..9fded1d0c13b5 100644
--- a/routers/web/repo/actions/badge.go
+++ b/routers/web/repo/actions/badge.go
@@ -39,7 +39,7 @@ func GetWorkflowBadge(ctx *context.Context) {
 
 	ctx.Data["Badge"] = b
 	ctx.RespHeader().Set("Content-Type", "image/svg+xml")
-	switch ctx.Req.URL.Query().Get("style") {
+	switch style {
 	case badge.StyleFlat:
 		ctx.HTML(http.StatusOK, "shared/actions/runner_badge_flat")
 	case badge.StyleFlatSquare:

From 1d846b9a8f3bc3c8588cd3c968c4fe2cfa84e64a Mon Sep 17 00:00:00 2001
From: bytedream <bytedream@protonmail.com>
Date: Mon, 31 Mar 2025 15:10:50 +0200
Subject: [PATCH 4/7] Add global slice with all styles

---
 modules/badge/badge.go            | 24 ++++++++++++++----------
 routers/web/devtest/devtest.go    |  3 +--
 routers/web/repo/actions/badge.go |  2 +-
 3 files changed, 16 insertions(+), 13 deletions(-)

diff --git a/modules/badge/badge.go b/modules/badge/badge.go
index d2fd2fdec63c8..e3a39b0efd74f 100644
--- a/modules/badge/badge.go
+++ b/modules/badge/badge.go
@@ -65,16 +65,20 @@ const (
 	DefaultStyle      = StyleFlat
 )
 
-var StatusColorMap = map[actions_model.Status]string{
-	actions_model.StatusSuccess:   "#4c1",    // Green
-	actions_model.StatusSkipped:   "#dfb317", // Yellow
-	actions_model.StatusUnknown:   "#97ca00", // Light Green
-	actions_model.StatusFailure:   "#e05d44", // Red
-	actions_model.StatusCancelled: "#fe7d37", // Orange
-	actions_model.StatusWaiting:   "#dfb317", // Yellow
-	actions_model.StatusRunning:   "#dfb317", // Yellow
-	actions_model.StatusBlocked:   "#dfb317", // Yellow
-}
+var (
+	StatusColorMap = map[actions_model.Status]string{
+		actions_model.StatusSuccess:   "#4c1",    // Green
+		actions_model.StatusSkipped:   "#dfb317", // Yellow
+		actions_model.StatusUnknown:   "#97ca00", // Light Green
+		actions_model.StatusFailure:   "#e05d44", // Red
+		actions_model.StatusCancelled: "#fe7d37", // Orange
+		actions_model.StatusWaiting:   "#dfb317", // Yellow
+		actions_model.StatusRunning:   "#dfb317", // Yellow
+		actions_model.StatusBlocked:   "#dfb317", // Yellow
+	}
+
+	AllStyles = []Style{StyleFlat, StyleFlatSquare}
+)
 
 // GenerateBadge generates badge with given template
 func GenerateBadge(label, message, color string) Badge {
diff --git a/routers/web/devtest/devtest.go b/routers/web/devtest/devtest.go
index 6c01b37bd7717..f1a1314f6619c 100644
--- a/routers/web/devtest/devtest.go
+++ b/routers/web/devtest/devtest.go
@@ -128,7 +128,6 @@ func prepareMockDataBadgeCommitSign(ctx *context.Context) {
 
 func prepareMockDataBadgeActionsSvg(ctx *context.Context) {
 	fontFamilyNames := strings.Split(badge.DefaultFontFamily, ",")
-	styles := []badge.Style{badge.StyleFlat, badge.StyleFlatSquare}
 	selectedFontFamilyName := ctx.FormString("font", fontFamilyNames[0])
 	selectedStyle := ctx.FormString("style", badge.DefaultStyle)
 	var badges []badge.Badge
@@ -163,7 +162,7 @@ func prepareMockDataBadgeActionsSvg(ctx *context.Context) {
 	ctx.Data["BadgeSVGs"] = badgeSVGs
 	ctx.Data["BadgeFontFamilyNames"] = fontFamilyNames
 	ctx.Data["SelectedFontFamilyName"] = selectedFontFamilyName
-	ctx.Data["BadgeStyles"] = styles
+	ctx.Data["BadgeStyles"] = badge.AllStyles
 	ctx.Data["SelectedStyle"] = selectedStyle
 }
 
diff --git a/routers/web/repo/actions/badge.go b/routers/web/repo/actions/badge.go
index 9fded1d0c13b5..4984bd45a17d8 100644
--- a/routers/web/repo/actions/badge.go
+++ b/routers/web/repo/actions/badge.go
@@ -27,7 +27,7 @@ func GetWorkflowBadge(ctx *context.Context) {
 	event := ctx.Req.URL.Query().Get("event")
 
 	style := ctx.Req.URL.Query().Get("style")
-	if !slices.Contains([]badge.Style{badge.StyleFlat, badge.StyleFlatSquare}, badge.Style(style)) {
+	if !slices.Contains(badge.AllStyles, badge.Style(style)) {
 		style = badge.DefaultStyle
 	}
 

From ad0f9e2ad8584fdc6af6975df8507e48aef08447 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Tue, 1 Apr 2025 09:58:47 +0800
Subject: [PATCH 5/7] optimize, lazy init, default style fallback

---
 modules/badge/badge.go             | 22 +++++++++++++---------
 modules/badge/badge_glyph_width.go |  6 ++----
 routers/web/devtest/devtest.go     |  2 +-
 routers/web/repo/actions/badge.go  | 26 +++++++++-----------------
 4 files changed, 25 insertions(+), 31 deletions(-)

diff --git a/modules/badge/badge.go b/modules/badge/badge.go
index e3a39b0efd74f..a1bd768805df5 100644
--- a/modules/badge/badge.go
+++ b/modules/badge/badge.go
@@ -5,6 +5,7 @@ package badge
 
 import (
 	"strings"
+	"sync"
 	"unicode"
 
 	actions_model "code.gitea.io/gitea/models/actions"
@@ -49,9 +50,6 @@ func (b Badge) Width() int {
 	return b.Label.width + b.Message.width
 }
 
-// Style represents the style of a badge
-type Style string
-
 const (
 	StyleFlat       = "flat"
 	StyleFlatSquare = "flat-square"
@@ -65,8 +63,13 @@ const (
 	DefaultStyle      = StyleFlat
 )
 
-var (
-	StatusColorMap = map[actions_model.Status]string{
+var GlobalVars = sync.OnceValue(func() (ret struct {
+	StatusColorMap       map[actions_model.Status]string
+	DejaVuGlyphWidthData map[rune]uint8
+	AllStyles            []string
+},
+) {
+	ret.StatusColorMap = map[actions_model.Status]string{
 		actions_model.StatusSuccess:   "#4c1",    // Green
 		actions_model.StatusSkipped:   "#dfb317", // Yellow
 		actions_model.StatusUnknown:   "#97ca00", // Light Green
@@ -76,9 +79,10 @@ var (
 		actions_model.StatusRunning:   "#dfb317", // Yellow
 		actions_model.StatusBlocked:   "#dfb317", // Yellow
 	}
-
-	AllStyles = []Style{StyleFlat, StyleFlatSquare}
-)
+	ret.DejaVuGlyphWidthData = dejaVuGlyphWidthDataFunc()
+	ret.AllStyles = []string{StyleFlat, StyleFlatSquare}
+	return ret
+})
 
 // GenerateBadge generates badge with given template
 func GenerateBadge(label, message, color string) Badge {
@@ -106,7 +110,7 @@ func GenerateBadge(label, message, color string) Badge {
 
 func calculateTextWidth(text string) int {
 	width := 0
-	widthData := DejaVuGlyphWidthData()
+	widthData := GlobalVars().DejaVuGlyphWidthData
 	for _, char := range strings.TrimSpace(text) {
 		charWidth, ok := widthData[char]
 		if !ok {
diff --git a/modules/badge/badge_glyph_width.go b/modules/badge/badge_glyph_width.go
index e8e43ec9cb950..0d950c5a70e7f 100644
--- a/modules/badge/badge_glyph_width.go
+++ b/modules/badge/badge_glyph_width.go
@@ -3,8 +3,6 @@
 
 package badge
 
-import "sync"
-
 // DejaVuGlyphWidthData is generated by `sfnt.Face.GlyphAdvance(nil, <rune>, 11, font.HintingNone)` with DejaVu Sans
 // v2.37 (https://github.com/dejavu-fonts/dejavu-fonts/releases/download/version_2_37/dejavu-sans-ttf-2.37.zip).
 //
@@ -13,7 +11,7 @@ import "sync"
 //
 // A devtest page "/devtest/badge-actions-svg" could be used to check the rendered images.
 
-var DejaVuGlyphWidthData = sync.OnceValue(func() map[rune]uint8 {
+func dejaVuGlyphWidthDataFunc() map[rune]uint8 {
 	return map[rune]uint8{
 		32:  3,
 		33:  4,
@@ -205,4 +203,4 @@ var DejaVuGlyphWidthData = sync.OnceValue(func() map[rune]uint8 {
 		254: 7,
 		255: 7,
 	}
-})
+}
diff --git a/routers/web/devtest/devtest.go b/routers/web/devtest/devtest.go
index f1a1314f6619c..995f9694264d1 100644
--- a/routers/web/devtest/devtest.go
+++ b/routers/web/devtest/devtest.go
@@ -162,7 +162,7 @@ func prepareMockDataBadgeActionsSvg(ctx *context.Context) {
 	ctx.Data["BadgeSVGs"] = badgeSVGs
 	ctx.Data["BadgeFontFamilyNames"] = fontFamilyNames
 	ctx.Data["SelectedFontFamilyName"] = selectedFontFamilyName
-	ctx.Data["BadgeStyles"] = badge.AllStyles
+	ctx.Data["BadgeStyles"] = badge.GlobalVars().AllStyles
 	ctx.Data["SelectedStyle"] = selectedStyle
 }
 
diff --git a/routers/web/repo/actions/badge.go b/routers/web/repo/actions/badge.go
index 4984bd45a17d8..d268a8df8af37 100644
--- a/routers/web/repo/actions/badge.go
+++ b/routers/web/repo/actions/badge.go
@@ -5,33 +5,25 @@ package actions
 
 import (
 	"errors"
-	"fmt"
 	"net/http"
 	"path/filepath"
-	"slices"
 	"strings"
 
 	actions_model "code.gitea.io/gitea/models/actions"
 	"code.gitea.io/gitea/modules/badge"
+	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/services/context"
 )
 
 func GetWorkflowBadge(ctx *context.Context) {
 	workflowFile := ctx.PathParam("workflow_name")
-	branch := ctx.Req.URL.Query().Get("branch")
-	if branch == "" {
-		branch = ctx.Repo.Repository.DefaultBranch
-	}
-	branchRef := fmt.Sprintf("refs/heads/%s", branch)
-	event := ctx.Req.URL.Query().Get("event")
-
-	style := ctx.Req.URL.Query().Get("style")
-	if !slices.Contains(badge.AllStyles, badge.Style(style)) {
-		style = badge.DefaultStyle
-	}
+	branch := ctx.FormString("branch", ctx.Repo.Repository.DefaultBranch)
+	event := ctx.FormString("event")
+	style := ctx.FormString("style")
 
-	b, err := getWorkflowBadge(ctx, workflowFile, branchRef, event)
+	branchRef := git.RefNameFromBranch(branch)
+	b, err := getWorkflowBadge(ctx, workflowFile, branchRef.String(), event)
 	if err != nil {
 		ctx.ServerError("GetWorkflowBadge", err)
 		return
@@ -40,10 +32,10 @@ func GetWorkflowBadge(ctx *context.Context) {
 	ctx.Data["Badge"] = b
 	ctx.RespHeader().Set("Content-Type", "image/svg+xml")
 	switch style {
-	case badge.StyleFlat:
-		ctx.HTML(http.StatusOK, "shared/actions/runner_badge_flat")
 	case badge.StyleFlatSquare:
 		ctx.HTML(http.StatusOK, "shared/actions/runner_badge_flat-square")
+	default: // defaults to badge.StyleFlat
+		ctx.HTML(http.StatusOK, "shared/actions/runner_badge_flat")
 	}
 }
 
@@ -59,7 +51,7 @@ func getWorkflowBadge(ctx *context.Context, workflowFile, branchName, event stri
 		return badge.Badge{}, err
 	}
 
-	color, ok := badge.StatusColorMap[run.Status]
+	color, ok := badge.GlobalVars().StatusColorMap[run.Status]
 	if !ok {
 		return badge.GenerateBadge(workflowName, "unknown status", badge.DefaultColor), nil
 	}

From 3da42bcc6824ae67c0ffb27470a6970af0f18e51 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Tue, 1 Apr 2025 17:14:14 +0800
Subject: [PATCH 6/7] add comment for style

---
 modules/badge/badge.go | 1 +
 1 file changed, 1 insertion(+)

diff --git a/modules/badge/badge.go b/modules/badge/badge.go
index a1bd768805df5..e27efa2e1399b 100644
--- a/modules/badge/badge.go
+++ b/modules/badge/badge.go
@@ -50,6 +50,7 @@ func (b Badge) Width() int {
 	return b.Label.width + b.Message.width
 }
 
+// styles follows https://shields.io/badges
 const (
 	StyleFlat       = "flat"
 	StyleFlatSquare = "flat-square"

From ed20f51351abf0b441e92270abd8acff6f71f47f Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Tue, 1 Apr 2025 17:16:01 +0800
Subject: [PATCH 7/7] Update modules/badge/badge.go

---
 modules/badge/badge.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/modules/badge/badge.go b/modules/badge/badge.go
index e27efa2e1399b..d2e9bd9d1bcc6 100644
--- a/modules/badge/badge.go
+++ b/modules/badge/badge.go
@@ -50,7 +50,7 @@ func (b Badge) Width() int {
 	return b.Label.width + b.Message.width
 }
 
-// styles follows https://shields.io/badges
+// Style follows https://shields.io/badges
 const (
 	StyleFlat       = "flat"
 	StyleFlatSquare = "flat-square"